@newrelic/browser-agent 1.296.0 → 1.297.0-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.
Files changed (44) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/cjs/common/constants/env.cdn.js +1 -1
  3. package/dist/cjs/common/constants/env.npm.js +1 -1
  4. package/dist/cjs/common/harvest/harvester.js +2 -1
  5. package/dist/cjs/features/session_replay/aggregate/index.js +10 -41
  6. package/dist/cjs/features/session_replay/instrument/index.js +4 -4
  7. package/dist/cjs/features/session_replay/shared/recorder-events.js +2 -2
  8. package/dist/cjs/features/session_replay/shared/recorder.js +41 -61
  9. package/dist/cjs/features/session_replay/shared/utils.js +0 -13
  10. package/dist/cjs/features/utils/aggregate-base.js +6 -5
  11. package/dist/cjs/features/utils/event-buffer.js +3 -2
  12. package/dist/esm/common/constants/env.cdn.js +1 -1
  13. package/dist/esm/common/constants/env.npm.js +1 -1
  14. package/dist/esm/common/harvest/harvester.js +2 -2
  15. package/dist/esm/features/session_replay/aggregate/index.js +10 -41
  16. package/dist/esm/features/session_replay/instrument/index.js +4 -4
  17. package/dist/esm/features/session_replay/shared/recorder-events.js +2 -2
  18. package/dist/esm/features/session_replay/shared/recorder.js +42 -62
  19. package/dist/esm/features/session_replay/shared/utils.js +0 -12
  20. package/dist/esm/features/utils/aggregate-base.js +6 -5
  21. package/dist/esm/features/utils/event-buffer.js +3 -2
  22. package/dist/types/common/harvest/harvester.d.ts +15 -0
  23. package/dist/types/common/harvest/harvester.d.ts.map +1 -1
  24. package/dist/types/features/session_replay/aggregate/index.d.ts +0 -1
  25. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  26. package/dist/types/features/session_replay/shared/recorder-events.d.ts +1 -1
  27. package/dist/types/features/session_replay/shared/recorder-events.d.ts.map +1 -1
  28. package/dist/types/features/session_replay/shared/recorder.d.ts +10 -8
  29. package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
  30. package/dist/types/features/session_replay/shared/utils.d.ts +0 -8
  31. package/dist/types/features/session_replay/shared/utils.d.ts.map +1 -1
  32. package/dist/types/features/utils/aggregate-base.d.ts +2 -0
  33. package/dist/types/features/utils/aggregate-base.d.ts.map +1 -1
  34. package/dist/types/features/utils/event-buffer.d.ts +2 -1
  35. package/dist/types/features/utils/event-buffer.d.ts.map +1 -1
  36. package/package.json +2 -2
  37. package/src/common/harvest/harvester.js +2 -2
  38. package/src/features/session_replay/aggregate/index.js +8 -35
  39. package/src/features/session_replay/instrument/index.js +1 -1
  40. package/src/features/session_replay/shared/recorder-events.js +2 -2
  41. package/src/features/session_replay/shared/recorder.js +39 -67
  42. package/src/features/session_replay/shared/utils.js +0 -13
  43. package/src/features/utils/aggregate-base.js +6 -4
  44. package/src/features/utils/event-buffer.js +3 -2
@@ -11,18 +11,13 @@ import { stylesheetEvaluator } from './stylesheet-evaluator';
11
11
  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
- import { buildNRMetaNode, customMasker } from './utils';
14
+ import { customMasker } from './utils';
15
15
  import { IDEAL_PAYLOAD_SIZE } from '../../../common/constants/agent-constants';
16
- import { AggregateBase } from '../../utils/aggregate-base';
17
16
  import { warn } from '../../../common/util/console';
18
17
  import { single } from '../../../common/util/invoke';
18
+ import { registerHandler } from '../../../common/event-emitter/register-handler';
19
+ const RRWEB_DATA_CHANNEL = 'rrweb-data';
19
20
  export class Recorder {
20
- /** Each page mutation or event will be stored (raw) in this array. This array will be cleared on each harvest */
21
- #events;
22
- /** Backlog used for a 2-part sliding window to guarantee a 15-30s buffer window */
23
- #backloggedEvents;
24
- /** array of recorder events -- Will be filled only if forced harvest was triggered and harvester does not exist */
25
- #preloaded;
26
21
  /** flag that if true, blocks events from being "stored". Only set to true when a full snapshot has incomplete nodes (only stylesheets ATM) */
27
22
  #fixing = false;
28
23
  #warnCSSOnce = single(() => warn(47)); // notifies user of potential replayer issue if fix_stylesheets is off
@@ -32,13 +27,12 @@ export class Recorder {
32
27
  this.parent = parent;
33
28
  /** A flag that can be set to false by failing conversions to stop the fetching process */
34
29
  this.shouldFix = this.parent.agentRef.init.session_replay.fix_stylesheets;
35
- /** Event Buffers */
36
- this.#events = new RecorderEvents(this.shouldFix);
37
- this.#backloggedEvents = new RecorderEvents(this.shouldFix);
38
- this.#preloaded = [new RecorderEvents(this.shouldFix)];
39
- /** The pointer to the current bucket holding rrweb events */
40
- this.currentBufferTarget = this.#events;
41
- /** Only set to true once a snapshot node has been processed. Used to block preload harvests from sending before we know we have a snapshot */
30
+
31
+ /** Each page mutation or event will be stored (raw) in this array. This array will be cleared on each harvest */
32
+ this.events = new RecorderEvents(this.shouldFix);
33
+ /** Backlog used for a 2-part sliding window to guarantee a 15-30s buffer window */
34
+ this.backloggedEvents = new RecorderEvents(this.shouldFix);
35
+ /** Only set to true once a snapshot node has been processed. Used to block harvests from sending before we know we have a snapshot */
42
36
  this.hasSeenSnapshot = false;
43
37
  /** Hold on to the last meta node, so that it can be re-inserted if the meta and snapshot nodes are broken up due to harvesting */
44
38
  this.lastMeta = false;
@@ -46,32 +40,27 @@ export class Recorder {
46
40
  this.stopRecording = () => {
47
41
  this.parent.agentRef.runtime.isRecording = false;
48
42
  };
43
+ registerHandler(RRWEB_DATA_CHANNEL, (event, isCheckout) => {
44
+ this.audit(event, isCheckout);
45
+ }, this.parent.featureName, this.parent.ee);
49
46
  }
50
47
  getEvents() {
51
- if (this.#preloaded[0]?.events.length) {
52
- return {
53
- ...this.#preloaded[0],
54
- events: this.#preloaded[0].events,
55
- payloadBytesEstimation: this.#preloaded[0].payloadBytesEstimation,
56
- type: 'preloaded'
57
- };
58
- }
59
48
  return {
60
- events: [...this.#backloggedEvents.events, ...this.#events.events].filter(x => x),
49
+ events: [...this.backloggedEvents.events, ...this.events.events].filter(x => x),
61
50
  type: 'standard',
62
- cycleTimestamp: Math.min(this.#backloggedEvents.cycleTimestamp, this.#events.cycleTimestamp),
63
- payloadBytesEstimation: this.#backloggedEvents.payloadBytesEstimation + this.#events.payloadBytesEstimation,
64
- hasError: this.#backloggedEvents.hasError || this.#events.hasError,
65
- hasMeta: this.#backloggedEvents.hasMeta || this.#events.hasMeta,
66
- hasSnapshot: this.#backloggedEvents.hasSnapshot || this.#events.hasSnapshot,
67
- inlinedAllStylesheets: !!this.#backloggedEvents.events.length && this.#backloggedEvents.inlinedAllStylesheets || this.#events.inlinedAllStylesheets
51
+ cycleTimestamp: Math.min(this.backloggedEvents.cycleTimestamp, this.events.cycleTimestamp),
52
+ payloadBytesEstimation: this.backloggedEvents.payloadBytesEstimation + this.events.payloadBytesEstimation,
53
+ hasError: this.backloggedEvents.hasError || this.events.hasError,
54
+ hasMeta: this.backloggedEvents.hasMeta || this.events.hasMeta,
55
+ hasSnapshot: this.backloggedEvents.hasSnapshot || this.events.hasSnapshot,
56
+ inlinedAllStylesheets: !!this.backloggedEvents.events.length && this.backloggedEvents.inlinedAllStylesheets || this.events.inlinedAllStylesheets
68
57
  };
69
58
  }
70
59
 
71
- /** Clears the buffer (this.#events), and resets all payload metadata properties */
60
+ /** Clears the buffer (this.events), and resets all payload metadata properties */
72
61
  clearBuffer() {
73
- if (this.#preloaded[0]?.events.length) this.#preloaded.shift();else if (this.parent.mode === MODE.ERROR) this.#backloggedEvents = this.#events;else this.#backloggedEvents = new RecorderEvents(this.shouldFix);
74
- this.#events = new RecorderEvents(this.shouldFix);
62
+ this.backloggedEvents = this.parent.mode === MODE.ERROR ? this.events : new RecorderEvents(this.shouldFix);
63
+ this.events = new RecorderEvents(this.shouldFix);
75
64
  }
76
65
 
77
66
  /** Begin recording using configured recording lib */
@@ -94,7 +83,9 @@ export class Recorder {
94
83
  let stop;
95
84
  try {
96
85
  stop = recorder({
97
- emit: this.audit.bind(this),
86
+ emit: (event, isCheckout) => {
87
+ handle(RRWEB_DATA_CHANNEL, [event, isCheckout], undefined, this.parent.featureName, this.parent.ee);
88
+ },
98
89
  blockClass: block_class,
99
90
  ignoreClass: ignore_class,
100
91
  maskTextClass: mask_text_class,
@@ -132,7 +123,7 @@ export class Recorder {
132
123
  /** only run the full fixing behavior (more costly) if fix_stylesheets is configured as on (default behavior) */
133
124
  if (!this.shouldFix) {
134
125
  if (incompletes > 0) {
135
- this.currentBufferTarget.inlinedAllStylesheets = false;
126
+ this.events.inlinedAllStylesheets = false;
136
127
  this.#warnCSSOnce();
137
128
  handle(SUPPORTABILITY_METRIC_CHANNEL, [missingInlineSMTag + 'Skipped', incompletes], undefined, FEATURE_NAMES.metrics, this.parent.ee);
138
129
  }
@@ -144,7 +135,7 @@ export class Recorder {
144
135
  /** wait for the evaluator to download/replace the incompletes' src code and then take a new snap */
145
136
  stylesheetEvaluator.fix().then(failedToFix => {
146
137
  if (failedToFix > 0) {
147
- this.currentBufferTarget.inlinedAllStylesheets = false;
138
+ this.events.inlinedAllStylesheets = false;
148
139
  this.shouldFix = false;
149
140
  }
150
141
  handle(SUPPORTABILITY_METRIC_CHANNEL, [missingInlineSMTag + 'Failed', failedToFix], undefined, FEATURE_NAMES.metrics, this.parent.ee);
@@ -158,15 +149,12 @@ export class Recorder {
158
149
  if (!this.#fixing) this.store(event, isCheckout);
159
150
  }
160
151
 
161
- /** Store a payload in the buffer (this.#events). This should be the callback to the recording lib noticing a mutation */
152
+ /** Store a payload in the buffer (this.events). This should be the callback to the recording lib noticing a mutation */
162
153
  store(event, isCheckout) {
163
- if (!event) return;
164
- if (!(this.parent instanceof AggregateBase) && this.#preloaded.length) this.currentBufferTarget = this.#preloaded[this.#preloaded.length - 1];else this.currentBufferTarget = this.#events;
165
- if (this.parent.blocked) return;
166
- if (this.parent.timeKeeper?.ready && !event.__newrelic) {
167
- event.__newrelic = buildNRMetaNode(event.timestamp, this.parent.timeKeeper);
168
- event.timestamp = this.parent.timeKeeper.correctAbsoluteTimestamp(event.timestamp);
169
- }
154
+ if (!event || this.parent.blocked) return;
155
+
156
+ /** because we've waited until draining to process the buffered rrweb events, we can guarantee the timekeeper exists */
157
+ event.timestamp = this.parent.timeKeeper.correctAbsoluteTimestamp(event.timestamp);
170
158
  event.__serialized = stringify(event);
171
159
  const eventBytes = event.__serialized.length;
172
160
  /** The estimated size of the payload after compression */
@@ -181,26 +169,18 @@ export class Recorder {
181
169
  }
182
170
 
183
171
  // meta event
184
- if (event.type === RRWEB_EVENT_TYPES.Meta) {
185
- this.currentBufferTarget.hasMeta = true;
186
- }
172
+ this.events.hasMeta ||= event.type === RRWEB_EVENT_TYPES.Meta;
187
173
  // snapshot event
188
- if (event.type === RRWEB_EVENT_TYPES.FullSnapshot) {
189
- this.currentBufferTarget.hasSnapshot = true;
190
- this.hasSeenSnapshot = true;
191
- }
192
- this.currentBufferTarget.add(event);
174
+ this.events.hasSnapshot ||= this.hasSeenSnapshot ||= event.type === RRWEB_EVENT_TYPES.FullSnapshot;
175
+
176
+ //* dont let the EventBuffer class double evaluate the event data size, it's a performance burden and we have special reasons to do it outside the event buffer */
177
+ this.events.add(event, eventBytes);
193
178
 
194
179
  // We are making an effort to try to keep payloads manageable for unloading. If they reach the unload limit before their interval,
195
180
  // it will send immediately. This often happens on the first snapshot, which can be significantly larger than the other payloads.
196
- if ((event.type === RRWEB_EVENT_TYPES.FullSnapshot && this.currentBufferTarget.hasMeta || payloadSize > IDEAL_PAYLOAD_SIZE) && this.parent.mode === MODE.FULL) {
197
- // if we've made it to the ideal size of ~64kb before the interval timer, we should send early.
198
- if (this.parent instanceof AggregateBase) {
199
- this.parent.agentRef.runtime.harvester.triggerHarvestFor(this.parent);
200
- } else {
201
- // we are still in "preload" and it triggered a "stop point". Make a new set, which will get pointed at on next cycle
202
- this.#preloaded.push(new RecorderEvents(this.shouldFix));
203
- }
181
+ if ((this.events.hasSnapshot && this.events.hasMeta || payloadSize > IDEAL_PAYLOAD_SIZE) && this.parent.mode === MODE.FULL) {
182
+ // if we've made it to the ideal size of ~16kb before the interval timer, we should send early.
183
+ this.parent.agentRef.runtime.harvester.triggerHarvestFor(this.parent);
204
184
  }
205
185
  }
206
186
 
@@ -214,13 +194,13 @@ export class Recorder {
214
194
  }
215
195
  }
216
196
  clearTimestamps() {
217
- this.currentBufferTarget.cycleTimestamp = undefined;
197
+ this.events.cycleTimestamp = undefined;
218
198
  }
219
199
 
220
200
  /** Estimate the payload size */
221
201
  getPayloadSize(newBytes = 0) {
222
202
  // the query param padding constant gives us some padding for the other metadata to be safely injected
223
- return this.estimateCompression(this.currentBufferTarget.payloadBytesEstimation + newBytes) + QUERY_PARAM_PADDING;
203
+ return this.estimateCompression(this.events.payloadBytesEstimation + newBytes) + QUERY_PARAM_PADDING;
224
204
  }
225
205
 
226
206
  /** Extensive research has yielded about an 88% compression factor on these payloads.
@@ -4,7 +4,6 @@
4
4
  */
5
5
  import { gosNREUMOriginals } from '../../../common/window/nreum';
6
6
  import { canEnableSessionTracking } from '../../utils/feature-gates';
7
- import { originTime } from '../../../common/constants/runtime';
8
7
  export function hasReplayPrerequisite(agentInit) {
9
8
  return !!gosNREUMOriginals().o.MO &&
10
9
  // Session Replay cannot work without Mutation Observer
@@ -15,17 +14,6 @@ export function hasReplayPrerequisite(agentInit) {
15
14
  export function isPreloadAllowed(agentInit) {
16
15
  return agentInit?.session_replay.preload === true && hasReplayPrerequisite(agentInit);
17
16
  }
18
- export function buildNRMetaNode(timestamp, timeKeeper) {
19
- const correctedTimestamp = timeKeeper.correctAbsoluteTimestamp(timestamp);
20
- return {
21
- originalTimestamp: timestamp,
22
- correctedTimestamp,
23
- timestampDiff: timestamp - correctedTimestamp,
24
- originTime,
25
- correctedOriginTime: timeKeeper.correctedOriginTime,
26
- originTimeDiff: Math.floor(originTime - timeKeeper.correctedOriginTime)
27
- };
28
- }
29
17
  export function customMasker(text, element) {
30
18
  try {
31
19
  if (typeof element?.type === 'string') {
@@ -34,8 +34,9 @@ export class AggregateBase extends FeatureBase {
34
34
  /** @type {Boolean} indicates if custom attributes are combined in each event payload for size estimation purposes. this is set to true in derived classes that need to evaluate custom attributes separately from the event payload */
35
35
  this.customAttributesAreSeparate = false;
36
36
  /** @type {Boolean} indicates if the feature can harvest early. This is set to false in derived classes that need to block early harvests, like ajax under certain conditions */
37
- this.canHarvestEarly = true; // this is set to false in derived classes that need to block early harvests, like ajax under certain conditions
38
-
37
+ this.canHarvestEarly = true;
38
+ /** @type {Boolean} indicates if the feature is actively in a retry deferral period */
39
+ this.isRetrying = false;
39
40
  this.harvestOpts = {}; // features aggregate classes can define custom opts for when their harvest is called
40
41
 
41
42
  const agentEntityGuid = this.agentRef?.runtime?.appMetadata?.agents?.[0]?.entityGuid;
@@ -83,7 +84,7 @@ export class AggregateBase extends FeatureBase {
83
84
  * @returns void
84
85
  */
85
86
  decideEarlyHarvest() {
86
- if (!this.canHarvestEarly) return;
87
+ if (!this.canHarvestEarly || this.blocked || this.isRetrying) return;
87
88
  const estimatedSize = this.events.byteSize() + (this.customAttributesAreSeparate ? this.agentRef.runtime.jsAttributesMetadata.bytes : 0);
88
89
  if (estimatedSize > IDEAL_PAYLOAD_SIZE) {
89
90
  this.agentRef.runtime.harvester.triggerHarvestFor(this);
@@ -172,8 +173,8 @@ export class AggregateBase extends FeatureBase {
172
173
  * @param {boolean=} result.retry - whether the harvest should be retried
173
174
  */
174
175
  postHarvestCleanup(result = {}) {
175
- const harvestFailed = result.sent && result.retry;
176
- if (harvestFailed) this.events.reloadSave(this.harvestOpts, result.targetApp?.entityGuid);
176
+ this.isRetrying = result.sent && result.retry;
177
+ if (this.isRetrying) this.events.reloadSave(this.harvestOpts, result.targetApp?.entityGuid);
177
178
  this.events.clearSave(this.harvestOpts, result.targetApp?.entityGuid);
178
179
  }
179
180
 
@@ -38,10 +38,11 @@ export class EventBuffer {
38
38
  /**
39
39
  * Add feature-processed event to our buffer. If this event would cause our total raw size to exceed the set max payload size, it is dropped.
40
40
  * @param {any} event - any primitive type or object
41
+ * @param {number} [evaluatedSize] - the evalated size of the event, if already done so before storing in the event buffer
41
42
  * @returns {Boolean} true if successfully added; false otherwise
42
43
  */
43
- add(event) {
44
- const addSize = stringify(event)?.length || 0; // (estimate) # of bytes a directly stringified event it would take to send
44
+ add(event, evaluatedSize) {
45
+ const addSize = evaluatedSize || stringify(event)?.length || 0; // (estimate) # of bytes a directly stringified event it would take to send
45
46
  if (this.#rawBytes + addSize > this.maxPayloadSize) {
46
47
  const smTag = inject => "EventBuffer/".concat(inject, "/Dropped/Bytes");
47
48
  this.featureAgg?.reportSupportabilityMetric(smTag(this.featureAgg.featureName), addSize); // bytes dropped for this feature will aggregate with this metric tag
@@ -1,3 +1,18 @@
1
+ /**
2
+ * Initiate a harvest call.
3
+ * @param {NetworkSendSpec} param0 Specification for sending data
4
+ * @returns {boolean} True if a network call was made. Note that this does not mean or guarantee that it was successful.
5
+ */
6
+ export function send(agentRef: any, { endpoint, targetApp, payload, localOpts, submitMethod, cbFinished, raw, featureName }: {
7
+ endpoint: any;
8
+ targetApp: any;
9
+ payload: any;
10
+ localOpts?: {} | undefined;
11
+ submitMethod: any;
12
+ cbFinished: any;
13
+ raw: any;
14
+ featureName: any;
15
+ }): boolean;
1
16
  export class Harvester {
2
17
  constructor(agentRef: any);
3
18
  initializedAggregates: any[];
@@ -1 +1 @@
1
- {"version":3,"file":"harvester.d.ts","sourceRoot":"","sources":["../../../../src/common/harvest/harvester.js"],"names":[],"mappings":"AAuBA;IAIE,2BAUC;IAZD,6BAA0B;IAGxB,cAAwB;IAW1B,wCASC;IAED;;;;;OAKG;IACH,iCAJW,MAAM,cACN,MAAM,GACJ,OAAO,CA+CnB;;CACF;8BAGY,OAAO,YAAY,EAAE,eAAe"}
1
+ {"version":3,"file":"harvester.d.ts","sourceRoot":"","sources":["../../../../src/common/harvest/harvester.js"],"names":[],"mappings":"AA6GA;;;;IAII;AACJ;;;;;;;;;IAFc,OAAO,CA+FpB;AAxLD;IAIE,2BAUC;IAZD,6BAA0B;IAGxB,cAAwB;IAW1B,wCASC;IAED;;;;;OAKG;IACH,iCAJW,MAAM,cACN,MAAM,GACJ,OAAO,CA+CnB;;CACF;8BAGY,OAAO,YAAY,EAAE,eAAe"}
@@ -30,7 +30,6 @@ export class Aggregate extends AggregateBase {
30
30
  targetApp: undefined;
31
31
  payload: undefined;
32
32
  }[] | undefined;
33
- getCorrectedTimestamp(node: any): any;
34
33
  /**
35
34
  * returns the timestamps for the earliest and latest nodes in the provided array, even if out of order
36
35
  * @param {Object[]} [nodes] - the nodes to evaluate
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_replay/aggregate/index.js"],"names":[],"mappings":"AAyBA;IACE,2BAAiC;IAIjC,sCAsFC;IAzFD,aAAe;IAKb,iFAAiF;IACjF,qBAAwB;IAGxB,2CAA2C;IAC3C,sDAAwB;IACxB,6CAA6C;IAC7C,gDAAmB;IAEnB,0BAA0B;IAC1B,kBAAqB;IACrB,6CAA6C;IAC7C,gBAA2B;IAE3B,cAA8B;IAC9B,kBAA+C;IAG/C,kCAAqG;IAoEvG,0BAEC;IAED,0BAMC;IAED,qBAUC;IAED;;;;;OAKG;IACH,4BAJW,OAAO,iBACP,OAAO,GACL,IAAI,CAuDhB;IAED,2BASC;IAED;;;oBAuDC;IAED,sCAIC;IAED;;;;OAIG;IACH,6BAHW,MAAM,EAAE,GACN;QAAE,UAAU,EAAE,MAAM,GAAC,SAAS,CAAC;QAAC,SAAS,EAAE,MAAM,GAAC,SAAS,CAAA;KAAE,CAUzE;IAED;;;;;;;;;;MAsEC;IAED,sCAKC;IAED;;;;OAIG;IACH,mCAKC;IAED,yDAAyD;IACzD,+CASC;IAED,yCAIC;CACF;8BAtY6B,4BAA4B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_replay/aggregate/index.js"],"names":[],"mappings":"AAwBA;IACE,2BAAiC;IAIjC,sCAqFC;IAxFD,aAAe;IAKb,iFAAiF;IACjF,qBAAwB;IAGxB,2CAA2C;IAC3C,sDAAwB;IACxB,6CAA6C;IAC7C,gDAAmB;IAEnB,0BAA0B;IAC1B,kBAAqB;IACrB,6CAA6C;IAC7C,gBAA2B;IAE3B,cAA8B;IAC9B,kBAA+C;IAG/C,kCAAqG;IAmEvG,0BAEC;IAED,0BAMC;IAED,qBAUC;IAED;;;;;OAKG;IACH,4BAJW,OAAO,iBACP,OAAO,GACL,IAAI,CAkDhB;IAED,2BASC;IAED;;;oBAyCC;IAED;;;;OAIG;IACH,6BAHW,MAAM,EAAE,GACN;QAAE,UAAU,EAAE,MAAM,GAAC,SAAS,CAAC;QAAC,SAAS,EAAE,MAAM,GAAC,SAAS,CAAA;KAAE,CAUzE;IAED;;;;;;;;;;MAsEC;IAED,sCAKC;IAED;;;;OAIG;IACH,mCAKC;IAED,yDAAyD;IACzD,+CASC;IAED,yCAIC;CACF;8BA3W6B,4BAA4B"}
@@ -15,7 +15,7 @@ export class RecorderEvents {
15
15
  hasError: boolean;
16
16
  /** Payload metadata -- Denotes whether all stylesheet elements were able to be inlined */
17
17
  inlinedAllStylesheets: boolean;
18
- add(event: any): void;
18
+ add(event: any, evaluatedSize: any): void;
19
19
  get events(): any[];
20
20
  /** A value which increments with every new mutation node reported. Resets after a harvest is sent */
21
21
  get payloadBytesEstimation(): number;
@@ -1 +1 @@
1
- {"version":3,"file":"recorder-events.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_replay/shared/recorder-events.js"],"names":[],"mappings":"AAMA;IAiBE,+CAGC;IAjBD;;SAEK;IACL,uBAA2B;IAC3B;;;MAGE;IACF,qBAAmB;IACnB,4IAA4I;IAC5I,iBAAe;IACf,+HAA+H;IAC/H,kBAAgB;IAGd,0FAA0F;IAC1F,+BAAoD;IAGtD,sBAEC;IAED,oBAEC;IAED,qGAAqG;IACrG,qCAEC;;CACF"}
1
+ {"version":3,"file":"recorder-events.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_replay/shared/recorder-events.js"],"names":[],"mappings":"AAMA;IAiBE,+CAGC;IAjBD;;SAEK;IACL,uBAA2B;IAC3B;;;MAGE;IACF,qBAAmB;IACnB,4IAA4I;IAC5I,iBAAe;IACf,+HAA+H;IAC/H,kBAAgB;IAGd,0FAA0F;IAC1F,+BAAoD;IAGtD,0CAEC;IAED,oBAEC;IAED,qGAAqG;IACrG,qCAEC;;CACF"}
@@ -4,9 +4,11 @@ export class Recorder {
4
4
  parent: any;
5
5
  /** A flag that can be set to false by failing conversions to stop the fetching process */
6
6
  shouldFix: any;
7
- /** The pointer to the current bucket holding rrweb events */
8
- currentBufferTarget: RecorderEvents;
9
- /** Only set to true once a snapshot node has been processed. Used to block preload harvests from sending before we know we have a snapshot */
7
+ /** Each page mutation or event will be stored (raw) in this array. This array will be cleared on each harvest */
8
+ events: RecorderEvents;
9
+ /** Backlog used for a 2-part sliding window to guarantee a 15-30s buffer window */
10
+ backloggedEvents: RecorderEvents;
11
+ /** Only set to true once a snapshot node has been processed. Used to block harvests from sending before we know we have a snapshot */
10
12
  hasSeenSnapshot: boolean;
11
13
  /** Hold on to the last meta node, so that it can be re-inserted if the meta and snapshot nodes are broken up due to harvesting */
12
14
  lastMeta: boolean;
@@ -14,15 +16,15 @@ export class Recorder {
14
16
  stopRecording: () => void;
15
17
  getEvents(): {
16
18
  events: any[];
17
- payloadBytesEstimation: number;
18
19
  type: string;
19
20
  cycleTimestamp: number;
20
- hasSnapshot: boolean;
21
- hasMeta: boolean;
21
+ payloadBytesEstimation: number;
22
22
  hasError: boolean;
23
+ hasMeta: boolean;
24
+ hasSnapshot: boolean;
23
25
  inlinedAllStylesheets: boolean;
24
26
  };
25
- /** Clears the buffer (this.#events), and resets all payload metadata properties */
27
+ /** Clears the buffer (this.events), and resets all payload metadata properties */
26
28
  clearBuffer(): void;
27
29
  /** Begin recording using configured recording lib */
28
30
  startRecording(): void;
@@ -33,7 +35,7 @@ export class Recorder {
33
35
  * @param {*} isCheckout - Flag indicating if the payload was triggered as a checkout
34
36
  */
35
37
  audit(event: any, isCheckout: any): void;
36
- /** Store a payload in the buffer (this.#events). This should be the callback to the recording lib noticing a mutation */
38
+ /** Store a payload in the buffer (this.events). This should be the callback to the recording lib noticing a mutation */
37
39
  store(event: any, isCheckout: any): void;
38
40
  /** force the recording lib to take a full DOM snapshot. This needs to occur in certain cases, like visibility changes */
39
41
  takeFullSnapshot(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"recorder.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_replay/shared/recorder.js"],"names":[],"mappings":"AAmBA;IAYE,yBAiBC;IAhBC,sDAAsD;IACtD,YAAoB;IACpB,0FAA0F;IAC1F,eAAyE;IAKzE,6DAA6D;IAC7D,oCAAuC;IACvC,+IAA+I;IAC/I,yBAA4B;IAC5B,kIAAkI;IAClI,kBAAqB;IACrB,uIAAuI;IACvI,0BAA+E;IAGjF;;;;;;;;;MAmBC;IAED,mFAAmF;IACnF,oBAKC;IAED,qDAAqD;IACrD,uBAiCC;IAED;;;;;OAKG;IACH,aAHW,GAAC,cACD,GAAC,QAiCX;IAED,0HAA0H;IAC1H,yCA+CC;IAED,0HAA0H;IAC1H,yBAOC;IAED,wBAEC;IAED,gCAAgC;IAChC,uCAGC;IAED;;;SAGK;IACL,oCAGC;;CACF;+BAjO8B,mBAAmB"}
1
+ {"version":3,"file":"recorder.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_replay/shared/recorder.js"],"names":[],"mappings":"AAqBA;IAME,yBAkBC;IAjBC,sDAAsD;IACtD,YAAoB;IACpB,0FAA0F;IAC1F,eAAyE;IAEzE,iHAAiH;IACjH,uBAAgD;IAChD,mFAAmF;IACnF,iCAA0D;IAC1D,uIAAuI;IACvI,yBAA4B;IAC5B,kIAAkI;IAClI,kBAAqB;IACrB,uIAAuI;IACvI,0BAA+E;IAKjF;;;;;;;;;MAWC;IAED,kFAAkF;IAClF,oBAGC;IAED,qDAAqD;IACrD,uBAiCC;IAED;;;;;OAKG;IACH,aAHW,GAAC,cACD,GAAC,QAiCX;IAED,yHAAyH;IACzH,yCAgCC;IAED,0HAA0H;IAC1H,yBAOC;IAED,wBAEC;IAED,gCAAgC;IAChC,uCAGC;IAED;;;SAGK;IACL,oCAGC;;CACF;+BArM8B,mBAAmB"}
@@ -1,12 +1,4 @@
1
1
  export function hasReplayPrerequisite(agentInit: any): boolean;
2
2
  export function isPreloadAllowed(agentInit: any): boolean;
3
- export function buildNRMetaNode(timestamp: any, timeKeeper: any): {
4
- originalTimestamp: any;
5
- correctedTimestamp: any;
6
- timestampDiff: number;
7
- originTime: number;
8
- correctedOriginTime: any;
9
- originTimeDiff: number;
10
- };
11
3
  export function customMasker(text: any, element: any): any;
12
4
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_replay/shared/utils.js"],"names":[],"mappings":"AAQA,+DAIC;AAED,0DAEC;AAED;;;;;;;EAUC;AAED,2DAUC"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_replay/shared/utils.js"],"names":[],"mappings":"AAOA,+DAIC;AAED,0DAEC;AAED,2DAUC"}
@@ -10,6 +10,8 @@ export class AggregateBase extends FeatureBase {
10
10
  customAttributesAreSeparate: boolean;
11
11
  /** @type {Boolean} indicates if the feature can harvest early. This is set to false in derived classes that need to block early harvests, like ajax under certain conditions */
12
12
  canHarvestEarly: boolean;
13
+ /** @type {Boolean} indicates if the feature is actively in a retry deferral period */
14
+ isRetrying: boolean;
13
15
  harvestOpts: {};
14
16
  events: any;
15
17
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"aggregate-base.d.ts","sourceRoot":"","sources":["../../../../src/features/utils/aggregate-base.js"],"names":[],"mappings":"AAsBA;IACE;;;;OAIG;IACH,sBAHW,MAAM,eACN,MAAM,EAyBhB;IArBC,iBAAwB;IAIxB,uOAAuO;IACvO,qCAAwC;IACxC,gLAAgL;IAChL,yBAA2B;IAE3B,gBAAqB;IA4BjB,YAAwJ;IAW9J;;;;OAIG;IACH,2BAOC;IAED;;;;OAIG;IACH,yBAHW,MAAM,EAAE,gBAwBlB;IAED;;OAEG;IACH,cAGC;IADC,6BAAmB;IAGrB,qCAEC;IAED;;;;;;OAMG;IACH,uDAJW,MAAM,GAAC,SAAS,SAyB1B;IAED;;;;;;;OAOG;IACH,4BALG;QAAwB,SAAS,GAAzB,MAAM,YAAC;KACf,QAQF;IAED;;;OAGG;IACH,6CAsBC;IAED;;;OAGG;IACH,2CAOC;IALC,gBAA6C;IAO/C;;;;OAIG;IACH,uCAHW,GAAC,UACD,GAAC,QAIX;;CACF;4BA7N2B,gBAAgB"}
1
+ {"version":3,"file":"aggregate-base.d.ts","sourceRoot":"","sources":["../../../../src/features/utils/aggregate-base.js"],"names":[],"mappings":"AAsBA;IACE;;;;OAIG;IACH,sBAHW,MAAM,eACN,MAAM,EA2BhB;IAvBC,iBAAwB;IAIxB,uOAAuO;IACvO,qCAAwC;IACxC,gLAAgL;IAChL,yBAA2B;IAC3B,sFAAsF;IACtF,oBAAuB;IAEvB,gBAAqB;IA4BjB,YAAwJ;IAW9J;;;;OAIG;IACH,2BAOC;IAED;;;;OAIG;IACH,yBAHW,MAAM,EAAE,gBAwBlB;IAED;;OAEG;IACH,cAGC;IADC,6BAAmB;IAGrB,qCAEC;IAED;;;;;;OAMG;IACH,uDAJW,MAAM,GAAC,SAAS,SAyB1B;IAED;;;;;;;OAOG;IACH,4BALG;QAAwB,SAAS,GAAzB,MAAM,YAAC;KACf,QAQF;IAED;;;OAGG;IACH,6CAsBC;IAED;;;OAGG;IACH,2CAOC;IALC,gBAA6C;IAO/C;;;;OAIG;IACH,uCAHW,GAAC,UACD,GAAC,QAIX;;CACF;4BA/N2B,gBAAgB"}
@@ -15,9 +15,10 @@ export class EventBuffer {
15
15
  /**
16
16
  * Add feature-processed event to our buffer. If this event would cause our total raw size to exceed the set max payload size, it is dropped.
17
17
  * @param {any} event - any primitive type or object
18
+ * @param {number} [evaluatedSize] - the evalated size of the event, if already done so before storing in the event buffer
18
19
  * @returns {Boolean} true if successfully added; false otherwise
19
20
  */
20
- add(event: any): boolean;
21
+ add(event: any, evaluatedSize?: number): boolean;
21
22
  /**
22
23
  * Merges events in the buffer that match the given criteria.
23
24
  * @param {Function} matcher - A function that takes an event and returns true if it should be merged.
@@ -1 +1 @@
1
- {"version":3,"file":"event-buffer.d.ts","sourceRoot":"","sources":["../../../../src/features/utils/event-buffer.js"],"names":[],"mappings":"AAOA;IAME;;;;OAIG;IACH,kDAFW,MAAM,EAKhB;IAFC,uBAAoC;IACpC,+BAA4B;IAG9B,qBAEC;IAED,mBAEC;IAED,aAEC;IAED,mBAEC;IAED,+CAEC;IAED;;;;OAIG;IACH,WAHW,GAAG,WAeb;IAED;;;;;KAKC;IACD,+BAHS,MAAM,GACJ,OAAO,CAWjB;IAED;;;;;;;OAOG;IACH,aALG;QAAsB,eAAe;QACf,YAAY;QACZ,gBAAgB;KACtC,GAAU,IAAI,CAWhB;IAED;;OAEG;IACH,aAGC;IAED;;OAEG;IACH,kBAGC;IAED;;OAEG;IACH,mBAKC;;CACF"}
1
+ {"version":3,"file":"event-buffer.d.ts","sourceRoot":"","sources":["../../../../src/features/utils/event-buffer.js"],"names":[],"mappings":"AAOA;IAME;;;;OAIG;IACH,kDAFW,MAAM,EAKhB;IAFC,uBAAoC;IACpC,+BAA4B;IAG9B,qBAEC;IAED,mBAEC;IAED,aAEC;IAED,mBAEC;IAED,+CAEC;IAED;;;;;OAKG;IACH,WAJW,GAAG,kBACH,MAAM,WAehB;IAED;;;;;KAKC;IACD,+BAHS,MAAM,GACJ,OAAO,CAWjB;IAED;;;;;;;OAOG;IACH,aALG;QAAsB,eAAe;QACf,YAAY;QACZ,gBAAgB;KACtC,GAAU,IAAI,CAWhB;IAED;;OAEG;IACH,aAGC;IAED;;OAEG;IACH,kBAGC;IAED;;OAEG;IACH,mBAKC;;CACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newrelic/browser-agent",
3
- "version": "1.296.0",
3
+ "version": "1.297.0-rc.1",
4
4
  "private": false,
5
5
  "author": "New Relic Browser Agent Team <browser-agent@newrelic.com>",
6
6
  "description": "New Relic Browser Agent",
@@ -295,4 +295,4 @@
295
295
  "README.md",
296
296
  "CHANGELOG.md"
297
297
  ]
298
- }
298
+ }
@@ -112,7 +112,7 @@ const warnings = {}
112
112
  * @param {NetworkSendSpec} param0 Specification for sending data
113
113
  * @returns {boolean} True if a network call was made. Note that this does not mean or guarantee that it was successful.
114
114
  */
115
- function send (agentRef, { endpoint, targetApp, payload, localOpts = {}, submitMethod, cbFinished, raw, featureName }) {
115
+ export function send (agentRef, { endpoint, targetApp, payload, localOpts = {}, submitMethod, cbFinished, raw, featureName }) {
116
116
  if (!agentRef.info.errorBeacon) return false
117
117
 
118
118
  let { body, qs } = cleanPayload(payload)
@@ -218,7 +218,7 @@ function send (agentRef, { endpoint, targetApp, payload, localOpts = {}, submitM
218
218
  function cleanPayload (payload = {}) {
219
219
  const clean = (input) => {
220
220
  if ((typeof Uint8Array !== 'undefined' && input instanceof Uint8Array) || Array.isArray(input)) return input
221
- if (typeof input === 'string') return input.length > 0 ? input : null
221
+ if (typeof input === 'string') return input
222
222
  return Object.entries(input || {}).reduce((accumulator, [key, value]) => {
223
223
  if ((typeof value === 'number') ||
224
224
  (typeof value === 'string' && value.length > 0) ||
@@ -18,7 +18,6 @@ import { MODE, SESSION_EVENTS, SESSION_EVENT_TYPES } from '../../../common/sessi
18
18
  import { stringify } from '../../../common/util/stringify'
19
19
  import { stylesheetEvaluator } from '../shared/stylesheet-evaluator'
20
20
  import { now } from '../../../common/timing/now'
21
- import { buildNRMetaNode } from '../shared/utils'
22
21
  import { MAX_PAYLOAD_SIZE } from '../../../common/constants/agent-constants'
23
22
  import { cleanURL } from '../../../common/url/clean-url'
24
23
  import { canEnableSessionTracking } from '../../utils/feature-gates'
@@ -94,8 +93,7 @@ export class Aggregate extends AggregateBase {
94
93
  }
95
94
  return
96
95
  }
97
- this.drain()
98
- this.initializeRecording(srMode)
96
+ this.initializeRecording(srMode).then(() => { this.drain() })
99
97
  }).then(() => {
100
98
  if (this.mode === MODE.OFF) {
101
99
  this.recorder?.stopRecording() // stop any conservative preload recording launched by instrument
@@ -121,7 +119,7 @@ export class Aggregate extends AggregateBase {
121
119
  }
122
120
 
123
121
  handleError (e) {
124
- if (this.recorder) this.recorder.currentBufferTarget.hasError = true
122
+ if (this.recorder) this.recorder.events.hasError = true
125
123
  // run once
126
124
  if (this.mode === MODE.ERROR && globalScope?.document.visibilityState === 'visible') {
127
125
  this.switchToFull()
@@ -171,10 +169,10 @@ export class Aggregate extends AggregateBase {
171
169
 
172
170
  if (!this.recorder) {
173
171
  try {
174
- // Do not change the webpackChunkName or it will break the webpack nrba-chunking plugin
172
+ // Do not change the webpackChunkName or it will break the webpack nrba-chunking plugin
175
173
  const { Recorder } = (await import(/* webpackChunkName: "recorder" */'../shared/recorder'))
176
174
  this.recorder = new Recorder(this)
177
- this.recorder.currentBufferTarget.hasError = this.errorNoticed
175
+ this.recorder.events.hasError = this.errorNoticed
178
176
  } catch (err) {
179
177
  return this.abort(ABORT_REASONS.IMPORT)
180
178
  }
@@ -189,11 +187,6 @@ export class Aggregate extends AggregateBase {
189
187
  // ERROR mode will do this until an error is thrown, and then switch into FULL mode.
190
188
  // The makeHarvestPayload should ensure that no payload is returned if we're not in FULL mode...
191
189
 
192
- // If theres preloaded events and we are in full mode, just harvest immediately to clear up space and for consistency
193
- if (this.mode === MODE.FULL && this.recorder?.getEvents().type === 'preloaded') {
194
- this.prepUtils().then(() => this.agentRef.runtime.harvester.triggerHarvestFor(this))
195
- }
196
-
197
190
  await this.prepUtils()
198
191
 
199
192
  if (!this.agentRef.runtime.isRecording) this.recorder.startRecording()
@@ -231,24 +224,10 @@ export class Aggregate extends AggregateBase {
231
224
 
232
225
  let len = 0
233
226
  if (!!this.gzipper && !!this.u8) {
234
- payload.body = this.gzipper(this.u8(`[${payload.body.map(({ __serialized, ...e }) => {
235
- if (e.__newrelic && __serialized) return __serialized
236
- const output = { ...e }
237
- if (!output.__newrelic) {
238
- output.__newrelic = buildNRMetaNode(e.timestamp, this.timeKeeper)
239
- output.timestamp = this.timeKeeper.correctAbsoluteTimestamp(e.timestamp)
240
- }
241
- return stringify(output)
242
- }).join(',')}]`))
227
+ payload.body = this.gzipper(this.u8(`[${payload.body.map(({ __serialized }) => (__serialized)).join(',')}]`))
243
228
  len = payload.body.length
244
229
  } else {
245
- payload.body = payload.body.map(({ __serialized, ...node }) => {
246
- if (node.__newrelic) return node
247
- const output = { ...node }
248
- output.__newrelic = buildNRMetaNode(node.timestamp, this.timeKeeper)
249
- output.timestamp = this.timeKeeper.correctAbsoluteTimestamp(node.timestamp)
250
- return output
251
- })
230
+ for (let idx in payload.body) delete payload.body[idx].__serialized
252
231
  len = stringify(payload.body).length
253
232
  }
254
233
 
@@ -269,12 +248,6 @@ export class Aggregate extends AggregateBase {
269
248
  return [payloadOutput]
270
249
  }
271
250
 
272
- getCorrectedTimestamp (node) {
273
- if (!node?.timestamp) return
274
- if (node.__newrelic) return node.timestamp
275
- return this.timeKeeper.correctAbsoluteTimestamp(node.timestamp)
276
- }
277
-
278
251
  /**
279
252
  * returns the timestamps for the earliest and latest nodes in the provided array, even if out of order
280
253
  * @param {Object[]} [nodes] - the nodes to evaluate
@@ -318,8 +291,8 @@ export class Aggregate extends AggregateBase {
318
291
 
319
292
  const { firstEvent, lastEvent } = this.getFirstAndLastNodes(events)
320
293
  // from rrweb node || from when the harvest cycle started
321
- const firstTimestamp = this.getCorrectedTimestamp(firstEvent) || Math.floor(this.timeKeeper.correctAbsoluteTimestamp(recorderEvents.cycleTimestamp))
322
- const lastTimestamp = this.getCorrectedTimestamp(lastEvent) || Math.floor(this.timeKeeper.correctRelativeTimestamp(relativeNow))
294
+ const firstTimestamp = firstEvent?.timestamp || Math.floor(this.timeKeeper.correctAbsoluteTimestamp(recorderEvents.cycleTimestamp))
295
+ const lastTimestamp = lastEvent?.timestamp || Math.floor(this.timeKeeper.correctRelativeTimestamp(relativeNow))
323
296
 
324
297
  const agentMetadata = agentRuntime.appMetadata?.agents?.[0] || {}
325
298
 
@@ -78,7 +78,7 @@ export class Instrument extends InstrumentBase {
78
78
 
79
79
  // If startReplay() has been used by this point, we must record in full mode regardless of session preload:
80
80
  // Note: recorder starts here with w/e the mode is at this time, but this may be changed later (see #apiStartOrRestartReplay else-case)
81
- this.recorder ??= new Recorder({ mode: this.#mode, agentIdentifier: this.agentIdentifier, trigger, ee: this.ee, agentRef: this.#agentRef })
81
+ this.recorder ??= new Recorder({ ...this, mode: this.#mode, agentRef: this.#agentRef, trigger, timeKeeper: this.#agentRef.runtime.timeKeeper }) // if TK exists due to deferred state, pass it
82
82
  this.recorder.startRecording()
83
83
  this.abortHandler = this.recorder.stopRecording
84
84
  } catch (err) {
@@ -26,8 +26,8 @@ export class RecorderEvents {
26
26
  this.inlinedAllStylesheets = shouldInlineStylesheets
27
27
  }
28
28
 
29
- add (event) {
30
- this.#events.add(event)
29
+ add (event, evaluatedSize) {
30
+ this.#events.add(event, evaluatedSize)
31
31
  }
32
32
 
33
33
  get events () {