@newrelic/browser-agent 1.297.0 → 1.297.1-rc.1
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 +7 -0
- package/README.md +3 -0
- package/dist/cjs/common/constants/agent-constants.js +3 -2
- package/dist/cjs/common/constants/env.cdn.js +1 -1
- package/dist/cjs/common/constants/env.npm.js +1 -1
- package/dist/cjs/common/harvest/types.js +0 -1
- package/dist/cjs/common/wrap/wrap-function.js +9 -4
- package/dist/cjs/features/ajax/aggregate/index.js +10 -2
- package/dist/cjs/features/ajax/instrument/index.js +1 -0
- package/dist/cjs/features/jserrors/aggregate/index.js +9 -4
- package/dist/cjs/features/session_replay/aggregate/index.js +18 -21
- package/dist/cjs/features/session_replay/constants.js +5 -1
- package/dist/cjs/features/session_replay/instrument/index.js +36 -50
- package/dist/cjs/features/session_replay/shared/recorder.js +47 -25
- package/dist/cjs/features/soft_navigations/aggregate/ajax-node.js +11 -3
- package/dist/cjs/features/soft_navigations/aggregate/index.js +38 -14
- package/dist/cjs/features/soft_navigations/aggregate/interaction.js +34 -20
- package/dist/cjs/features/soft_navigations/constants.js +8 -4
- package/dist/cjs/features/soft_navigations/instrument/index.js +9 -6
- package/dist/cjs/features/utils/instrument-base.js +6 -2
- package/dist/cjs/loaders/api/interaction-types.js +0 -1
- package/dist/cjs/loaders/micro-agent.js +5 -2
- package/dist/esm/common/constants/agent-constants.js +2 -1
- package/dist/esm/common/constants/env.cdn.js +1 -1
- package/dist/esm/common/constants/env.npm.js +1 -1
- package/dist/esm/common/harvest/types.js +0 -1
- package/dist/esm/common/wrap/wrap-function.js +9 -4
- package/dist/esm/features/ajax/aggregate/index.js +10 -2
- package/dist/esm/features/ajax/instrument/index.js +1 -0
- package/dist/esm/features/jserrors/aggregate/index.js +9 -4
- package/dist/esm/features/session_replay/aggregate/index.js +18 -21
- package/dist/esm/features/session_replay/constants.js +5 -1
- package/dist/esm/features/session_replay/instrument/index.js +36 -50
- package/dist/esm/features/session_replay/shared/recorder.js +48 -26
- package/dist/esm/features/soft_navigations/aggregate/ajax-node.js +11 -3
- package/dist/esm/features/soft_navigations/aggregate/index.js +39 -15
- package/dist/esm/features/soft_navigations/aggregate/interaction.js +35 -21
- package/dist/esm/features/soft_navigations/constants.js +7 -3
- package/dist/esm/features/soft_navigations/instrument/index.js +10 -7
- package/dist/esm/features/utils/instrument-base.js +6 -2
- package/dist/esm/loaders/api/interaction-types.js +0 -1
- package/dist/esm/loaders/micro-agent.js +5 -2
- package/dist/types/common/constants/agent-constants.d.ts +1 -0
- package/dist/types/common/constants/agent-constants.d.ts.map +1 -1
- package/dist/types/common/harvest/types.d.ts.map +1 -1
- package/dist/types/common/wrap/wrap-function.d.ts.map +1 -1
- package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/ajax/instrument/index.d.ts.map +1 -1
- package/dist/types/features/jserrors/aggregate/index.d.ts +1 -1
- package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/aggregate/index.d.ts +9 -2
- 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/instrument/index.d.ts +7 -0
- package/dist/types/features/session_replay/instrument/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/recorder.d.ts +10 -4
- package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
- package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts +2 -1
- package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts.map +1 -1
- package/dist/types/features/soft_navigations/aggregate/index.d.ts +1 -1
- package/dist/types/features/soft_navigations/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/soft_navigations/aggregate/interaction.d.ts +6 -3
- package/dist/types/features/soft_navigations/aggregate/interaction.d.ts.map +1 -1
- package/dist/types/features/soft_navigations/constants.d.ts +4 -0
- package/dist/types/features/soft_navigations/constants.d.ts.map +1 -1
- package/dist/types/features/soft_navigations/instrument/index.d.ts.map +1 -1
- package/dist/types/features/utils/instrument-base.d.ts +2 -1
- package/dist/types/features/utils/instrument-base.d.ts.map +1 -1
- package/dist/types/loaders/api/interaction-types.d.ts.map +1 -1
- package/dist/types/loaders/micro-agent.d.ts +5 -2
- package/dist/types/loaders/micro-agent.d.ts.map +1 -1
- package/package.json +2 -6
- package/src/common/constants/agent-constants.js +1 -0
- package/src/common/harvest/types.js +0 -1
- package/src/common/wrap/wrap-function.js +9 -4
- package/src/features/ajax/aggregate/index.js +10 -2
- package/src/features/ajax/instrument/index.js +1 -0
- package/src/features/jserrors/aggregate/index.js +10 -6
- package/src/features/session_replay/aggregate/index.js +18 -19
- package/src/features/session_replay/constants.js +5 -1
- package/src/features/session_replay/instrument/index.js +39 -40
- package/src/features/session_replay/shared/recorder.js +53 -26
- package/src/features/soft_navigations/aggregate/ajax-node.js +8 -4
- package/src/features/soft_navigations/aggregate/index.js +39 -15
- package/src/features/soft_navigations/aggregate/interaction.js +33 -19
- package/src/features/soft_navigations/constants.js +5 -2
- package/src/features/soft_navigations/instrument/index.js +9 -8
- package/src/features/utils/instrument-base.js +7 -2
- package/src/loaders/api/interaction-types.js +0 -1
- package/src/loaders/micro-agent.js +5 -2
|
@@ -41,8 +41,9 @@ export class Aggregate extends AggregateBase {
|
|
|
41
41
|
this.entitled = false;
|
|
42
42
|
/** set at BCS response, stored in runtime */
|
|
43
43
|
this.timeKeeper = undefined;
|
|
44
|
-
this.
|
|
45
|
-
this.
|
|
44
|
+
this.instrumentClass = args;
|
|
45
|
+
// point this var here just in case it already exists and can be used by APIs (session pause, resume, etc.) before handling rum flags
|
|
46
|
+
this.recorder = this.instrumentClass?.recorder;
|
|
46
47
|
this.harvestOpts.raw = true;
|
|
47
48
|
this.isSessionTrackingEnabled = canEnableSessionTracking(agentRef.init) && !!agentRef.runtime.session;
|
|
48
49
|
this.reportSupportabilityMetric('Config/SessionReplay/Enabled');
|
|
@@ -62,7 +63,7 @@ export class Aggregate extends AggregateBase {
|
|
|
62
63
|
// if the mode changed on a different tab, it needs to update this instance to match
|
|
63
64
|
this.mode = agentRef.runtime.session.state.sessionReplayMode;
|
|
64
65
|
if (!this.initialized || this.mode === MODE.OFF) return;
|
|
65
|
-
this.recorder?.startRecording();
|
|
66
|
+
this.recorder?.startRecording(TRIGGERS.RESUME, this.mode);
|
|
66
67
|
});
|
|
67
68
|
this.ee.on(SESSION_EVENTS.UPDATE, (type, data) => {
|
|
68
69
|
if (!this.recorder || !this.initialized || this.blocked || type !== SESSION_EVENT_TYPES.CROSS_TAB) return;
|
|
@@ -131,7 +132,7 @@ export class Aggregate extends AggregateBase {
|
|
|
131
132
|
this.mode = MODE.FULL;
|
|
132
133
|
// if the error was noticed AFTER the recorder was already imported....
|
|
133
134
|
if (this.recorder && this.initialized) {
|
|
134
|
-
if (!this.agentRef.runtime.isRecording) this.recorder.startRecording();
|
|
135
|
+
if (!this.agentRef.runtime.isRecording) this.recorder.startRecording(TRIGGERS.SWITCH_TO_FULL, this.mode); // off --> full
|
|
135
136
|
this.syncWithSessionManager({
|
|
136
137
|
sessionReplayMode: this.mode
|
|
137
138
|
});
|
|
@@ -144,9 +145,10 @@ export class Aggregate extends AggregateBase {
|
|
|
144
145
|
* Evaluate entitlements and sampling before starting feature mechanics, importing and configuring recording library, and setting storage state
|
|
145
146
|
* @param {boolean} srMode - the true/false state of the "sr" flag (aka. entitlements) from RUM response
|
|
146
147
|
* @param {boolean} ignoreSession - whether to force the method to ignore the session state and use just the sample flags
|
|
148
|
+
* @param {TRIGGERS} [trigger=TRIGGERS.INITIALIZE] - the trigger that initiated the recording. Usually TRIGGERS.INITIALIZE, but could be TRIGGERS.API since in certain cases that trigger calls this method
|
|
147
149
|
* @returns {void}
|
|
148
150
|
*/
|
|
149
|
-
async initializeRecording(srMode, ignoreSession) {
|
|
151
|
+
async initializeRecording(srMode, ignoreSession, trigger = TRIGGERS.INITIALIZE) {
|
|
150
152
|
this.initialized = true;
|
|
151
153
|
if (!this.entitled) return;
|
|
152
154
|
|
|
@@ -161,7 +163,7 @@ export class Aggregate extends AggregateBase {
|
|
|
161
163
|
timeKeeper
|
|
162
164
|
} = this.agentRef.runtime;
|
|
163
165
|
this.timeKeeper = timeKeeper;
|
|
164
|
-
if (this.recorder?.
|
|
166
|
+
if (this.recorder?.trigger === TRIGGERS.API && this.agentRef.runtime.isRecording) {
|
|
165
167
|
this.mode = MODE.FULL;
|
|
166
168
|
} else if (!session.isNew && !ignoreSession) {
|
|
167
169
|
// inherit the mode of the existing session
|
|
@@ -172,30 +174,25 @@ export class Aggregate extends AggregateBase {
|
|
|
172
174
|
}
|
|
173
175
|
// If off, then don't record (early return)
|
|
174
176
|
if (this.mode === MODE.OFF) return;
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
this.recorder = new Recorder(this);
|
|
182
|
-
this.recorder.events.hasError = this.errorNoticed;
|
|
183
|
-
} catch (err) {
|
|
184
|
-
return this.abort(ABORT_REASONS.IMPORT);
|
|
185
|
-
}
|
|
186
|
-
} else {
|
|
187
|
-
this.recorder.parent = this;
|
|
177
|
+
try {
|
|
178
|
+
/** will return a recorder instance if already imported, otherwise, will fetch the recorder and initialize it */
|
|
179
|
+
this.recorder ??= await this.instrumentClass.importRecorder();
|
|
180
|
+
} catch (err) {
|
|
181
|
+
/** if the recorder fails to import, abort the feature */
|
|
182
|
+
return this.abort(ABORT_REASONS.IMPORT, err);
|
|
188
183
|
}
|
|
189
184
|
|
|
190
185
|
// If an error was noticed before the mode could be set (like in the early lifecycle of the page), immediately set to FULL mode
|
|
191
|
-
if (this.mode === MODE.ERROR && this.errorNoticed)
|
|
186
|
+
if (this.mode === MODE.ERROR && this.instrumentClass.errorNoticed) {
|
|
187
|
+
this.mode = MODE.FULL;
|
|
188
|
+
}
|
|
192
189
|
|
|
193
190
|
// FULL mode records AND reports from the beginning, while ERROR mode only records (but does not report).
|
|
194
191
|
// ERROR mode will do this until an error is thrown, and then switch into FULL mode.
|
|
195
192
|
// The makeHarvestPayload should ensure that no payload is returned if we're not in FULL mode...
|
|
196
193
|
|
|
197
194
|
await this.prepUtils();
|
|
198
|
-
if (!this.agentRef.runtime.isRecording) this.recorder.startRecording();
|
|
195
|
+
if (!this.agentRef.runtime.isRecording) this.recorder.startRecording(trigger, this.mode);
|
|
199
196
|
this.syncWithSessionManager({
|
|
200
197
|
sessionReplayMode: this.mode
|
|
201
198
|
});
|
|
@@ -54,5 +54,9 @@ export const ABORT_REASONS = {
|
|
|
54
54
|
/** Reserved room for query param attrs */
|
|
55
55
|
export const QUERY_PARAM_PADDING = 5000;
|
|
56
56
|
export const TRIGGERS = {
|
|
57
|
-
API: 'api'
|
|
57
|
+
API: 'api',
|
|
58
|
+
RESUME: 'resume',
|
|
59
|
+
SWITCH_TO_FULL: 'switchToFull',
|
|
60
|
+
INITIALIZE: 'initialize',
|
|
61
|
+
PRELOAD: 'preload'
|
|
58
62
|
};
|
|
@@ -15,8 +15,10 @@ import { setupRecordReplayAPI } from '../../../loaders/api/recordReplay';
|
|
|
15
15
|
import { setupPauseReplayAPI } from '../../../loaders/api/pauseReplay';
|
|
16
16
|
export class Instrument extends InstrumentBase {
|
|
17
17
|
static featureName = FEATURE_NAME;
|
|
18
|
-
|
|
19
|
-
#
|
|
18
|
+
/** @type {Promise|undefined} A promise that resolves when the recorder module is imported and added to the class. Undefined if the recorder has never been staged to import with `importRecorder`. */
|
|
19
|
+
#stagedImport;
|
|
20
|
+
/** The RRWEB recorder instance, if imported */
|
|
21
|
+
recorder;
|
|
20
22
|
constructor(agentRef) {
|
|
21
23
|
super(agentRef, FEATURE_NAME);
|
|
22
24
|
|
|
@@ -24,7 +26,6 @@ export class Instrument extends InstrumentBase {
|
|
|
24
26
|
setupRecordReplayAPI(agentRef);
|
|
25
27
|
setupPauseReplayAPI(agentRef);
|
|
26
28
|
let session;
|
|
27
|
-
this.#agentRef = agentRef;
|
|
28
29
|
try {
|
|
29
30
|
session = JSON.parse(localStorage.getItem("".concat(PREFIX, "_").concat(DEFAULT_KEY)));
|
|
30
31
|
} catch (err) {}
|
|
@@ -32,15 +33,16 @@ export class Instrument extends InstrumentBase {
|
|
|
32
33
|
this.ee.on(SR_EVENT_EMITTER_TYPES.RECORD, () => this.#apiStartOrRestartReplay());
|
|
33
34
|
}
|
|
34
35
|
if (this.#canPreloadRecorder(session)) {
|
|
35
|
-
this
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
this.importAggregator(this.#agentRef, () => import(/* webpackChunkName: "session_replay-aggregate" */'../aggregate'));
|
|
36
|
+
this.importRecorder().then(recorder => {
|
|
37
|
+
recorder.startRecording(TRIGGERS.PRELOAD, session?.sessionReplayMode);
|
|
38
|
+
}); // could handle specific fail-state behaviors with a .catch block here
|
|
39
39
|
}
|
|
40
|
+
this.importAggregator(this.agentRef, () => import(/* webpackChunkName: "session_replay-aggregate" */'../aggregate'), this);
|
|
40
41
|
|
|
41
42
|
/** If the recorder is running, we can pass error events on to the agg to help it switch to full mode later */
|
|
42
43
|
this.ee.on('err', e => {
|
|
43
|
-
if (this
|
|
44
|
+
if (this.blocked) return;
|
|
45
|
+
if (this.agentRef.runtime.isRecording) {
|
|
44
46
|
this.errorNoticed = true;
|
|
45
47
|
handle(SR_EVENT_EMITTER_TYPES.ERROR_DURING_REPLAY, [e], undefined, this.featureName, this.ee);
|
|
46
48
|
}
|
|
@@ -53,66 +55,50 @@ export class Instrument extends InstrumentBase {
|
|
|
53
55
|
// this might be a new session if entity initializes: conservatively start recording if first-time config allows
|
|
54
56
|
// Note: users with SR enabled, as well as these other configs enabled by-default, will be penalized by the recorder overhead EVEN IF they don't actually have or get
|
|
55
57
|
// entitlement or sampling decision, or otherwise intentionally opted-in for the feature.
|
|
56
|
-
return isPreloadAllowed(this
|
|
58
|
+
return isPreloadAllowed(this.agentRef.init);
|
|
57
59
|
} else if (session.sessionReplayMode === MODE.FULL || session.sessionReplayMode === MODE.ERROR) {
|
|
58
60
|
return true; // existing sessions get to continue recording, regardless of this page's configs or if it has expired (conservatively)
|
|
59
61
|
} else {
|
|
60
62
|
// SR mode was OFF but may potentially be turned on if session resets and configs allows the new session to have replay...
|
|
61
|
-
return isPreloadAllowed(this
|
|
63
|
+
return isPreloadAllowed(this.agentRef.init);
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
|
-
|
|
66
|
+
|
|
65
67
|
/**
|
|
66
|
-
*
|
|
68
|
+
* Returns a promise that imports the recorder module. Only lets the recorder module be imported and instantiated once. Rejects if failed to import/instantiate.
|
|
69
|
+
* @returns {Promise}
|
|
67
70
|
*/
|
|
68
|
-
|
|
69
|
-
if
|
|
70
|
-
this
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
timeKeeper: this.#agentRef.runtime.timeKeeper
|
|
84
|
-
}); // if TK exists due to deferred state, pass it
|
|
85
|
-
this.recorder.startRecording();
|
|
86
|
-
this.abortHandler = this.recorder.stopRecording;
|
|
87
|
-
} catch (err) {
|
|
88
|
-
this.parent.ee.emit('internal-error', [err]);
|
|
89
|
-
}
|
|
90
|
-
this.importAggregator(this.#agentRef, () => import(/* webpackChunkName: "session_replay-aggregate" */'../aggregate'), {
|
|
91
|
-
recorder: this.recorder,
|
|
92
|
-
errorNoticed: this.errorNoticed
|
|
71
|
+
importRecorder() {
|
|
72
|
+
/** if we already have a recorder fully set up, just return it */
|
|
73
|
+
if (this.recorder) return Promise.resolve(this.recorder);
|
|
74
|
+
/** conditional -- if we have never started importing, stage the import and store it in state */
|
|
75
|
+
this.#stagedImport ??= import(/* webpackChunkName: "recorder" */'../shared/recorder').then(({
|
|
76
|
+
Recorder
|
|
77
|
+
}) => {
|
|
78
|
+
this.recorder = new Recorder(this);
|
|
79
|
+
/** return the recorder for promise chaining */
|
|
80
|
+
return this.recorder;
|
|
81
|
+
}).catch(err => {
|
|
82
|
+
this.ee.emit('internal-error', [err]);
|
|
83
|
+
this.blocked = true;
|
|
84
|
+
/** return the err for promise chaining */
|
|
85
|
+
throw err;
|
|
93
86
|
});
|
|
87
|
+
return this.#stagedImport;
|
|
94
88
|
}
|
|
95
89
|
|
|
96
90
|
/**
|
|
97
91
|
* Called whenever startReplay API is used. That could occur any time, pre or post load.
|
|
98
92
|
*/
|
|
99
93
|
#apiStartOrRestartReplay() {
|
|
94
|
+
if (this.blocked) return;
|
|
100
95
|
if (this.featAggregate) {
|
|
101
96
|
// post-load; there's possibly already an ongoing recording
|
|
102
|
-
if (this.featAggregate.mode !== MODE.FULL) this.featAggregate.initializeRecording(MODE.FULL, true);
|
|
97
|
+
if (this.featAggregate.mode !== MODE.FULL) this.featAggregate.initializeRecording(MODE.FULL, true, TRIGGERS.API);
|
|
103
98
|
} else {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
// There's a race here wherein either:
|
|
108
|
-
// a. Recorder has not been initialized, and we've set the enforced mode, so we're good, or;
|
|
109
|
-
// b. Record has been initialized, possibly with the "wrong" mode, so we have to correct that + restart.
|
|
110
|
-
if (this.recorder && this.recorder.parent.mode !== MODE.FULL) {
|
|
111
|
-
this.recorder.parent.mode = MODE.FULL;
|
|
112
|
-
this.recorder.stopRecording();
|
|
113
|
-
this.recorder.startRecording();
|
|
114
|
-
this.abortHandler = this.recorder.stopRecording;
|
|
115
|
-
}
|
|
99
|
+
this.importRecorder().then(() => {
|
|
100
|
+
this.recorder.startRecording(TRIGGERS.API, MODE.FULL);
|
|
101
|
+
}); // could handle specific fail-state behaviors with a .catch block here
|
|
116
102
|
}
|
|
117
103
|
}
|
|
118
104
|
}
|
|
@@ -12,7 +12,7 @@ import { handle } from '../../../common/event-emitter/handle';
|
|
|
12
12
|
import { SUPPORTABILITY_METRIC_CHANNEL } from '../../metrics/constants';
|
|
13
13
|
import { FEATURE_NAMES } from '../../../loaders/features/features';
|
|
14
14
|
import { customMasker } from './utils';
|
|
15
|
-
import { IDEAL_PAYLOAD_SIZE } from '../../../common/constants/agent-constants';
|
|
15
|
+
import { IDEAL_PAYLOAD_SIZE, SESSION_ERROR } from '../../../common/constants/agent-constants';
|
|
16
16
|
import { warn } from '../../../common/util/console';
|
|
17
17
|
import { single } from '../../../common/util/invoke';
|
|
18
18
|
import { registerHandler } from '../../../common/event-emitter/register-handler';
|
|
@@ -22,11 +22,19 @@ export class Recorder {
|
|
|
22
22
|
#fixing = false;
|
|
23
23
|
#warnCSSOnce = single(() => warn(47)); // notifies user of potential replayer issue if fix_stylesheets is off
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
#canRecord = true;
|
|
26
|
+
triggerHistory = []; // useful for debugging
|
|
27
|
+
|
|
28
|
+
constructor(srInstrument) {
|
|
29
|
+
/** The parent classes that share the recorder */
|
|
30
|
+
this.srInstrument = srInstrument;
|
|
31
|
+
// --- shortcuts
|
|
32
|
+
this.ee = srInstrument.ee;
|
|
33
|
+
this.srFeatureName = srInstrument.featureName;
|
|
34
|
+
this.agentRef = srInstrument.agentRef;
|
|
35
|
+
this.isErrorMode = false;
|
|
28
36
|
/** A flag that can be set to false by failing conversions to stop the fetching process */
|
|
29
|
-
this.shouldFix = this.
|
|
37
|
+
this.shouldFix = this.agentRef.init.session_replay.fix_stylesheets;
|
|
30
38
|
|
|
31
39
|
/** Each page mutation or event will be stored (raw) in this array. This array will be cleared on each harvest */
|
|
32
40
|
this.events = new RecorderEvents(this.shouldFix);
|
|
@@ -38,11 +46,18 @@ export class Recorder {
|
|
|
38
46
|
this.lastMeta = false;
|
|
39
47
|
/** The method to stop recording. This defaults to a noop, but is overwritten once the recording library is imported and initialized */
|
|
40
48
|
this.stopRecording = () => {
|
|
41
|
-
this.
|
|
49
|
+
this.agentRef.runtime.isRecording = false;
|
|
42
50
|
};
|
|
51
|
+
registerHandler(SESSION_ERROR, () => {
|
|
52
|
+
this.#canRecord = false;
|
|
53
|
+
this.stopRecording();
|
|
54
|
+
}, this.srFeatureName, this.ee);
|
|
43
55
|
registerHandler(RRWEB_DATA_CHANNEL, (event, isCheckout) => {
|
|
44
56
|
this.audit(event, isCheckout);
|
|
45
|
-
}, this.
|
|
57
|
+
}, this.srFeatureName, this.ee);
|
|
58
|
+
}
|
|
59
|
+
get trigger() {
|
|
60
|
+
return this.triggerHistory[this.triggerHistory.length - 1];
|
|
46
61
|
}
|
|
47
62
|
getEvents() {
|
|
48
63
|
return {
|
|
@@ -59,13 +74,20 @@ export class Recorder {
|
|
|
59
74
|
|
|
60
75
|
/** Clears the buffer (this.events), and resets all payload metadata properties */
|
|
61
76
|
clearBuffer() {
|
|
62
|
-
this.backloggedEvents = this.
|
|
77
|
+
this.backloggedEvents = this.isErrorMode ? this.events : new RecorderEvents(this.shouldFix);
|
|
63
78
|
this.events = new RecorderEvents(this.shouldFix);
|
|
64
79
|
}
|
|
65
80
|
|
|
66
81
|
/** Begin recording using configured recording lib */
|
|
67
|
-
startRecording() {
|
|
68
|
-
this
|
|
82
|
+
startRecording(trigger, mode) {
|
|
83
|
+
if (!this.#canRecord) return;
|
|
84
|
+
this.triggerHistory.push(trigger); // keep track of all triggers, useful for lifecycle debugging. "this.trigger" returns the latest entry
|
|
85
|
+
|
|
86
|
+
this.isErrorMode = mode === MODE.ERROR;
|
|
87
|
+
|
|
88
|
+
/** if the recorder is already recording... lets stop it before starting a new one */
|
|
89
|
+
this.stopRecording();
|
|
90
|
+
this.agentRef.runtime.isRecording = true;
|
|
69
91
|
const {
|
|
70
92
|
block_class,
|
|
71
93
|
ignore_class,
|
|
@@ -76,7 +98,7 @@ export class Recorder {
|
|
|
76
98
|
mask_all_inputs,
|
|
77
99
|
inline_images,
|
|
78
100
|
collect_fonts
|
|
79
|
-
} = this.
|
|
101
|
+
} = this.agentRef.init.session_replay;
|
|
80
102
|
|
|
81
103
|
// set up rrweb configurations for maximum privacy --
|
|
82
104
|
// https://newrelic.atlassian.net/wiki/spaces/O11Y/pages/2792293280/2023+02+28+Browser+-+Session+Replay#Configuration-options
|
|
@@ -84,7 +106,7 @@ export class Recorder {
|
|
|
84
106
|
try {
|
|
85
107
|
stop = recorder({
|
|
86
108
|
emit: (event, isCheckout) => {
|
|
87
|
-
handle(RRWEB_DATA_CHANNEL, [event, isCheckout], undefined, this.
|
|
109
|
+
handle(RRWEB_DATA_CHANNEL, [event, isCheckout], undefined, this.srFeatureName, this.ee);
|
|
88
110
|
},
|
|
89
111
|
blockClass: block_class,
|
|
90
112
|
ignoreClass: ignore_class,
|
|
@@ -98,14 +120,14 @@ export class Recorder {
|
|
|
98
120
|
inlineStylesheet: true,
|
|
99
121
|
inlineImages: inline_images,
|
|
100
122
|
collectFonts: collect_fonts,
|
|
101
|
-
checkoutEveryNms: CHECKOUT_MS[
|
|
123
|
+
checkoutEveryNms: CHECKOUT_MS[mode],
|
|
102
124
|
recordAfter: 'DOMContentLoaded'
|
|
103
125
|
});
|
|
104
126
|
} catch (err) {
|
|
105
|
-
this.
|
|
127
|
+
this.ee.emit('internal-error', [err]);
|
|
106
128
|
}
|
|
107
129
|
this.stopRecording = () => {
|
|
108
|
-
this.
|
|
130
|
+
this.agentRef.runtime.isRecording = false;
|
|
109
131
|
stop?.();
|
|
110
132
|
};
|
|
111
133
|
}
|
|
@@ -118,14 +140,14 @@ export class Recorder {
|
|
|
118
140
|
*/
|
|
119
141
|
audit(event, isCheckout) {
|
|
120
142
|
/** An count of stylesheet objects that were blocked from accessing contents via JS */
|
|
121
|
-
const incompletes = this.
|
|
143
|
+
const incompletes = this.agentRef.init.session_replay.fix_stylesheets ? stylesheetEvaluator.evaluate() : 0;
|
|
122
144
|
const missingInlineSMTag = 'SessionReplay/Payload/Missing-Inline-Css/';
|
|
123
145
|
/** only run the full fixing behavior (more costly) if fix_stylesheets is configured as on (default behavior) */
|
|
124
146
|
if (!this.shouldFix) {
|
|
125
147
|
if (incompletes > 0) {
|
|
126
148
|
this.events.inlinedAllStylesheets = false;
|
|
127
149
|
this.#warnCSSOnce();
|
|
128
|
-
handle(SUPPORTABILITY_METRIC_CHANNEL, [missingInlineSMTag + 'Skipped', incompletes], undefined, FEATURE_NAMES.metrics, this.
|
|
150
|
+
handle(SUPPORTABILITY_METRIC_CHANNEL, [missingInlineSMTag + 'Skipped', incompletes], undefined, FEATURE_NAMES.metrics, this.ee);
|
|
129
151
|
}
|
|
130
152
|
return this.store(event, isCheckout);
|
|
131
153
|
}
|
|
@@ -138,8 +160,8 @@ export class Recorder {
|
|
|
138
160
|
this.events.inlinedAllStylesheets = false;
|
|
139
161
|
this.shouldFix = false;
|
|
140
162
|
}
|
|
141
|
-
handle(SUPPORTABILITY_METRIC_CHANNEL, [missingInlineSMTag + 'Failed', failedToFix], undefined, FEATURE_NAMES.metrics, this.
|
|
142
|
-
handle(SUPPORTABILITY_METRIC_CHANNEL, [missingInlineSMTag + 'Fixed', incompletes - failedToFix], undefined, FEATURE_NAMES.metrics, this.
|
|
163
|
+
handle(SUPPORTABILITY_METRIC_CHANNEL, [missingInlineSMTag + 'Failed', failedToFix], undefined, FEATURE_NAMES.metrics, this.ee);
|
|
164
|
+
handle(SUPPORTABILITY_METRIC_CHANNEL, [missingInlineSMTag + 'Fixed', incompletes - failedToFix], undefined, FEATURE_NAMES.metrics, this.ee);
|
|
143
165
|
this.takeFullSnapshot();
|
|
144
166
|
});
|
|
145
167
|
/** Only start ignoring data if got a faulty snapshot */
|
|
@@ -151,10 +173,10 @@ export class Recorder {
|
|
|
151
173
|
|
|
152
174
|
/** Store a payload in the buffer (this.events). This should be the callback to the recording lib noticing a mutation */
|
|
153
175
|
store(event, isCheckout) {
|
|
154
|
-
if (!event || this.
|
|
176
|
+
if (!event || this.srInstrument.featAggregate?.blocked) return;
|
|
155
177
|
|
|
156
178
|
/** because we've waited until draining to process the buffered rrweb events, we can guarantee the timekeeper exists */
|
|
157
|
-
event.timestamp = this.
|
|
179
|
+
event.timestamp = this.agentRef.runtime.timeKeeper.correctAbsoluteTimestamp(event.timestamp);
|
|
158
180
|
event.__serialized = stringify(event);
|
|
159
181
|
const eventBytes = event.__serialized.length;
|
|
160
182
|
/** The estimated size of the payload after compression */
|
|
@@ -163,7 +185,7 @@ export class Recorder {
|
|
|
163
185
|
// to help reconstruct the replay later and must be included. While waiting and buffering for errors to come through,
|
|
164
186
|
// each time we see a new checkout, we can drop the old data.
|
|
165
187
|
// we need to check for meta because rrweb will flag it as checkout twice, once for meta, then once for snapshot
|
|
166
|
-
if (this.
|
|
188
|
+
if (this.isErrorMode && isCheckout && event.type === RRWEB_EVENT_TYPES.Meta) {
|
|
167
189
|
// we are still waiting for an error to throw, so keep wiping the buffer over time
|
|
168
190
|
this.clearBuffer();
|
|
169
191
|
}
|
|
@@ -178,16 +200,16 @@ export class Recorder {
|
|
|
178
200
|
|
|
179
201
|
// We are making an effort to try to keep payloads manageable for unloading. If they reach the unload limit before their interval,
|
|
180
202
|
// it will send immediately. This often happens on the first snapshot, which can be significantly larger than the other payloads.
|
|
181
|
-
if ((this.events.hasSnapshot && this.events.hasMeta || payloadSize > IDEAL_PAYLOAD_SIZE) && this.
|
|
203
|
+
if ((this.events.hasSnapshot && this.events.hasMeta || payloadSize > IDEAL_PAYLOAD_SIZE) && !this.isErrorMode) {
|
|
182
204
|
// if we've made it to the ideal size of ~16kb before the interval timer, we should send early.
|
|
183
|
-
this.
|
|
205
|
+
this.agentRef.runtime.harvester.triggerHarvestFor(this.srInstrument.featAggregate);
|
|
184
206
|
}
|
|
185
207
|
}
|
|
186
208
|
|
|
187
209
|
/** force the recording lib to take a full DOM snapshot. This needs to occur in certain cases, like visibility changes */
|
|
188
210
|
takeFullSnapshot() {
|
|
189
211
|
try {
|
|
190
|
-
if (!this.
|
|
212
|
+
if (!this.agentRef.runtime.isRecording) return;
|
|
191
213
|
recorder.takeFullSnapshot();
|
|
192
214
|
} catch (err) {
|
|
193
215
|
// in the off chance we think we are recording, but rrweb does not, rrweb's lib will throw an error. This catch is just a precaution
|
|
@@ -208,7 +230,7 @@ export class Recorder {
|
|
|
208
230
|
* https://staging.onenr.io/037jbJWxbjy
|
|
209
231
|
* */
|
|
210
232
|
estimateCompression(data) {
|
|
211
|
-
if (!!this.
|
|
233
|
+
if (!!this.srInstrument.featAggregate?.gzipper && !!this.srInstrument.featAggregate?.u8) return data * AVG_COMPRESSION;
|
|
212
234
|
return data;
|
|
213
235
|
}
|
|
214
236
|
}
|
|
@@ -6,7 +6,7 @@ import { addCustomAttributes, getAddStringContext, nullable, numeric } from '../
|
|
|
6
6
|
import { NODE_TYPE } from '../constants';
|
|
7
7
|
import { BelNode } from './bel-node';
|
|
8
8
|
export class AjaxNode extends BelNode {
|
|
9
|
-
constructor(ajaxEvent) {
|
|
9
|
+
constructor(ajaxEvent, ajaxContext) {
|
|
10
10
|
super();
|
|
11
11
|
this.belType = NODE_TYPE.AJAX;
|
|
12
12
|
this.method = ajaxEvent.method;
|
|
@@ -20,8 +20,12 @@ export class AjaxNode extends BelNode {
|
|
|
20
20
|
this.traceId = ajaxEvent.traceId;
|
|
21
21
|
this.spanTimestamp = ajaxEvent.spanTimestamp;
|
|
22
22
|
this.gql = ajaxEvent.gql;
|
|
23
|
-
this.start = ajaxEvent.startTime;
|
|
23
|
+
this.start = ajaxEvent.startTime;
|
|
24
24
|
this.end = ajaxEvent.endTime;
|
|
25
|
+
if (ajaxContext?.latestLongtaskEnd) {
|
|
26
|
+
this.callbackEnd = Math.max(ajaxContext.latestLongtaskEnd, this.end); // typically lt end if non-zero, but added clamping to end just in case
|
|
27
|
+
this.callbackDuration = this.callbackEnd - this.end; // callbackDuration is the time from ajax loaded to last long task observed from it
|
|
28
|
+
} else this.callbackEnd = this.end; // if no long task was observed, callbackEnd is the same as end
|
|
25
29
|
}
|
|
26
30
|
serialize(parentStartTimestamp, agentRef) {
|
|
27
31
|
const addString = getAddStringContext(agentRef.runtime.obfuscator);
|
|
@@ -34,7 +38,11 @@ export class AjaxNode extends BelNode {
|
|
|
34
38
|
// start relative to parent start (if part of first node in payload) or first parent start
|
|
35
39
|
numeric(this.end - this.start),
|
|
36
40
|
// end is relative to start
|
|
37
|
-
numeric(this.callbackEnd
|
|
41
|
+
numeric(this.callbackEnd - this.end),
|
|
42
|
+
// callbackEnd is relative to end
|
|
43
|
+
numeric(this.callbackDuration),
|
|
44
|
+
// not relative
|
|
45
|
+
addString(this.method), numeric(this.status), addString(this.domain), addString(this.path), numeric(this.txSize), numeric(this.rxSize), this.requestedWith, addString(this.nodeId), nullable(this.spanId, addString, true) + nullable(this.traceId, addString, true) + nullable(this.spanTimestamp, numeric)];
|
|
38
46
|
let allAttachedNodes = [];
|
|
39
47
|
if (typeof this.gql === 'object') allAttachedNodes = addCustomAttributes(this.gql, addString);
|
|
40
48
|
this.children.forEach(node => allAttachedNodes.push(node.serialize())); // no children is expected under ajax nodes at this time
|
|
@@ -8,7 +8,7 @@ import { single } from '../../../common/util/invoke';
|
|
|
8
8
|
import { timeToFirstByte } from '../../../common/vitals/time-to-first-byte';
|
|
9
9
|
import { FEATURE_NAMES } from '../../../loaders/features/features';
|
|
10
10
|
import { AggregateBase } from '../../utils/aggregate-base';
|
|
11
|
-
import { API_TRIGGER_NAME, FEATURE_NAME, INTERACTION_STATUS, INTERACTION_TRIGGERS, IPL_TRIGGER_NAME } from '../constants';
|
|
11
|
+
import { API_TRIGGER_NAME, FEATURE_NAME, INTERACTION_STATUS, INTERACTION_TRIGGERS, IPL_TRIGGER_NAME, NO_LONG_TASK_WINDOW, POPSTATE_MERGE_WINDOW, POPSTATE_TRIGGER } from '../constants';
|
|
12
12
|
import { AjaxNode } from './ajax-node';
|
|
13
13
|
import { InitialPageLoadInteraction } from './initial-page-load-interaction';
|
|
14
14
|
import { Interaction } from './interaction';
|
|
@@ -40,7 +40,7 @@ export class Aggregate extends AggregateBase {
|
|
|
40
40
|
});
|
|
41
41
|
this.latestRouteSetByApi = null;
|
|
42
42
|
this.interactionInProgress = null; // aside from the "page load" interaction, there can only ever be 1 ongoing at a time
|
|
43
|
-
this.latestHistoryUrl =
|
|
43
|
+
this.latestHistoryUrl = window.location.href; // the initial url is needed to get a correct oldURL in the case that the first nav is triggered by 'popstate'
|
|
44
44
|
this.harvestOpts.beforeUnload = () => this.interactionInProgress?.done(); // return any withheld ajax or jserr events so they can be sent with EoL harvest
|
|
45
45
|
|
|
46
46
|
this.waitForFlags(['spa']).then(([spaOn]) => {
|
|
@@ -56,14 +56,25 @@ export class Aggregate extends AggregateBase {
|
|
|
56
56
|
// By default, a complete UI driven interaction requires event -> URL change -> DOM mod in that exact order.
|
|
57
57
|
registerHandler('newUIEvent', event => this.startUIInteraction(event.type, Math.floor(event.timeStamp), event.target), this.featureName, this.ee);
|
|
58
58
|
registerHandler('newURL', (timestamp, url) => {
|
|
59
|
-
//
|
|
59
|
+
// The newURL always need to be tracked such that it becomes the oldURL of the next potential popstate ixn.
|
|
60
|
+
// Because for 'popstate' triggered newUIEVent, by the time the event fires, the page URL has already changed so the previous URL is lost if not recorded.
|
|
60
61
|
this.latestHistoryUrl = url;
|
|
61
62
|
this.interactionInProgress?.updateHistory(timestamp, url);
|
|
62
63
|
}, this.featureName, this.ee);
|
|
63
64
|
registerHandler('newDom', timestamp => {
|
|
64
65
|
this.interactionInProgress?.updateDom(timestamp);
|
|
65
|
-
|
|
66
|
+
this.interactionInProgress?.checkHistoryAndDomChange();
|
|
66
67
|
}, this.featureName, this.ee);
|
|
68
|
+
this.ee.on('long-task', task => {
|
|
69
|
+
if (!this.interactionInProgress?.watchLongtaskTimer) return; // no ixn in progress or it's not yet in a pending-finish state, as indicated by the lack of a watchLongtask timeout
|
|
70
|
+
clearTimeout(this.interactionInProgress.watchLongtaskTimer);
|
|
71
|
+
// Provided there isn't another long task, the ixn span will be extended to include this long task that would finish the interaction.
|
|
72
|
+
this.interactionInProgress.customEnd = task.end;
|
|
73
|
+
this.interactionInProgress.watchLongtaskTimer = setTimeout(() => this.interactionInProgress.done(), NO_LONG_TASK_WINDOW);
|
|
74
|
+
|
|
75
|
+
// Report metric on frequency of ixn extension due to long task
|
|
76
|
+
this.reportSupportabilityMetric('SoftNav/Interaction/Extended');
|
|
77
|
+
});
|
|
67
78
|
this.#registerApiHandlers();
|
|
68
79
|
registerHandler('ajax', this.#handleAjaxEvent.bind(this), this.featureName, this.ee);
|
|
69
80
|
registerHandler('jserror', this.#handleJserror.bind(this), this.featureName, this.ee);
|
|
@@ -82,9 +93,11 @@ export class Aggregate extends AggregateBase {
|
|
|
82
93
|
startUIInteraction(eventName, startedAt, sourceElem) {
|
|
83
94
|
// this is throttled by instrumentation so that it isn't excessively called
|
|
84
95
|
if (this.interactionInProgress?.createdByApi) return; // api-started interactions cannot be disrupted aka cancelled by UI events (and the vice versa applies as well)
|
|
85
|
-
|
|
96
|
+
// Navs from interacting with the document will emit the UI event like click, followed by a popstate which should be squashed given some margin of time. This prevents it from cancelling the first UI ixn.
|
|
97
|
+
if (eventName === POPSTATE_TRIGGER && this.interactionInProgress?.trigger !== POPSTATE_TRIGGER && startedAt - this.interactionInProgress?.start <= POPSTATE_MERGE_WINDOW) return;
|
|
98
|
+
if (this.interactionInProgress?.done() === false) return; // current in-progress is blocked from closing if true, e.g. by 'waitForEnd' api option; notice this cancels/finishes existing in-progress ixn
|
|
86
99
|
|
|
87
|
-
const oldURL = eventName ===
|
|
100
|
+
const oldURL = eventName === POPSTATE_TRIGGER ? this.latestHistoryUrl : undefined; // see related comment in 'newURL' handler above, 'popstate'
|
|
88
101
|
this.interactionInProgress = new Interaction(eventName, startedAt, this.latestRouteSetByApi, oldURL);
|
|
89
102
|
if (eventName === INTERACTION_TRIGGERS[0]) {
|
|
90
103
|
// 'click'
|
|
@@ -147,22 +160,30 @@ export class Aggregate extends AggregateBase {
|
|
|
147
160
|
/**
|
|
148
161
|
* Handles or redirect ajax event based on the interaction, if any, that it's tied to.
|
|
149
162
|
* @param {Object} event see Ajax feature's storeXhr function for object definition
|
|
163
|
+
* @param {Object} metadata reference to the ajax context, used to pass long task info
|
|
150
164
|
*/
|
|
151
|
-
#handleAjaxEvent(event) {
|
|
165
|
+
#handleAjaxEvent(event, metadata) {
|
|
152
166
|
const associatedInteraction = this.getInteractionFor(event.startTime);
|
|
153
167
|
if (!associatedInteraction) {
|
|
154
168
|
// no interaction was happening when this ajax started, so give it back to Ajax feature for processing
|
|
155
169
|
handle('returnAjax', [event], undefined, FEATURE_NAMES.ajax, this.ee);
|
|
156
170
|
} else {
|
|
157
|
-
if (associatedInteraction.status === INTERACTION_STATUS.FIN) processAjax(event, associatedInteraction); // tack ajax onto the ixn object awaiting harvest
|
|
171
|
+
if (associatedInteraction.status === INTERACTION_STATUS.FIN) processAjax.call(this, event, metadata, associatedInteraction); // tack ajax onto the ixn object awaiting harvest
|
|
158
172
|
else {
|
|
159
173
|
// 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
|
|
160
|
-
associatedInteraction.on('finished', () => processAjax(event, associatedInteraction));
|
|
174
|
+
associatedInteraction.on('finished', () => processAjax.call(this, event, metadata, associatedInteraction));
|
|
161
175
|
associatedInteraction.on('cancelled', () => handle('returnAjax', [event], undefined, FEATURE_NAMES.ajax, this.ee));
|
|
162
176
|
}
|
|
163
177
|
}
|
|
164
|
-
function processAjax(event, parentInteraction) {
|
|
165
|
-
const
|
|
178
|
+
function processAjax(event, metadata, parentInteraction) {
|
|
179
|
+
const finalEnd = parentInteraction.end; // assume: by the time the 'finished' event occurs & this executes, the ixn end time accounts for any long task extension + lookback window exclusion
|
|
180
|
+
if (event.startTime > finalEnd) {
|
|
181
|
+
handle('returnAjax', [event], undefined, FEATURE_NAMES.ajax, this.ee); // falling outside the final span, returned as standalone
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Metadata(ctx) should contain any long task end time associated with this XHR which should be up-to-date by the time the in-progress ixn & ajax children are being finalized for harvest.
|
|
186
|
+
const newNode = new AjaxNode(event, metadata);
|
|
166
187
|
parentInteraction.addChild(newNode);
|
|
167
188
|
}
|
|
168
189
|
}
|
|
@@ -185,7 +206,7 @@ export class Aggregate extends AggregateBase {
|
|
|
185
206
|
} else {
|
|
186
207
|
// These callbacks may be added multiple times for an ixn, but just a single run will deal with all jserrors associated with the interaction.
|
|
187
208
|
// 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.
|
|
188
|
-
associatedInteraction.on('finished', single(() => handle('softNavFlush', [associatedInteraction.id, true, associatedInteraction.customAttributes], undefined, FEATURE_NAMES.jserrors, this.ee)));
|
|
209
|
+
associatedInteraction.on('finished', single(() => handle('softNavFlush', [associatedInteraction.id, true, associatedInteraction.customAttributes, associatedInteraction.end], undefined, FEATURE_NAMES.jserrors, this.ee)));
|
|
189
210
|
associatedInteraction.on('cancelled', single(() => handle('softNavFlush', [associatedInteraction.id, false, undefined], undefined, FEATURE_NAMES.jserrors, this.ee))); // don't take custom attrs from cancelled ixns
|
|
190
211
|
}
|
|
191
212
|
}
|
|
@@ -200,7 +221,7 @@ export class Aggregate extends AggregateBase {
|
|
|
200
221
|
this.associatedInteraction = thisClass.getInteractionFor(time);
|
|
201
222
|
if (this.associatedInteraction?.trigger === IPL_TRIGGER_NAME) this.associatedInteraction = null; // the api get-interaction method cannot target IPL
|
|
202
223
|
if (!this.associatedInteraction) {
|
|
203
|
-
// This new api-driven interaction will be the target of any subsequent .interaction() call, until it is closed by EITHER .end() OR the regular
|
|
224
|
+
// This new api-driven interaction will be the target of any subsequent .interaction() call, until it is closed by EITHER .end() OR the regular url>dom change process.
|
|
204
225
|
this.associatedInteraction = thisClass.interactionInProgress = new Interaction(API_TRIGGER_NAME, time, thisClass.latestRouteSetByApi);
|
|
205
226
|
thisClass.domObserver.observe(document.body, {
|
|
206
227
|
attributes: true,
|
|
@@ -210,10 +231,13 @@ export class Aggregate extends AggregateBase {
|
|
|
210
231
|
}); // start observing for DOM changes like a regular UI-driven interaction
|
|
211
232
|
thisClass.setClosureHandlers();
|
|
212
233
|
}
|
|
213
|
-
if (waitForEnd === true)
|
|
234
|
+
if (waitForEnd === true) {
|
|
235
|
+
this.associatedInteraction.keepOpenUntilEndApi = true;
|
|
236
|
+
clearTimeout(this.associatedInteraction.cancellationTimer); // get rid of the auto-cancel 30s timer for UI ixns when users specify waitForEnd manual override
|
|
237
|
+
}
|
|
214
238
|
}, thisClass.featureName, thisClass.ee);
|
|
215
239
|
registerHandler(INTERACTION_API + 'end', function (timeNow) {
|
|
216
|
-
this.associatedInteraction.done(timeNow);
|
|
240
|
+
this.associatedInteraction.done(timeNow, true);
|
|
217
241
|
}, thisClass.featureName, thisClass.ee);
|
|
218
242
|
registerHandler(INTERACTION_API + 'save', function () {
|
|
219
243
|
this.associatedInteraction.forceSave = true;
|