@newrelic/browser-agent 1.256.1 → 1.257.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +16 -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/harvest.js +7 -5
  5. package/dist/cjs/features/jserrors/aggregate/index.js +25 -12
  6. package/dist/cjs/features/session_replay/aggregate/index.js +11 -5
  7. package/dist/cjs/features/session_replay/constants.js +2 -1
  8. package/dist/cjs/features/session_replay/instrument/index.js +15 -5
  9. package/dist/cjs/features/session_replay/shared/recorder.js +6 -3
  10. package/dist/cjs/features/session_replay/shared/utils.js +6 -5
  11. package/dist/cjs/features/soft_navigations/aggregate/index.js +2 -2
  12. package/dist/cjs/features/utils/instrument-base.js +11 -14
  13. package/dist/cjs/features/utils/nr1-debugger.js +27 -0
  14. package/dist/cjs/loaders/agent.js +4 -0
  15. package/dist/esm/common/constants/env.cdn.js +1 -1
  16. package/dist/esm/common/constants/env.npm.js +1 -1
  17. package/dist/esm/common/harvest/harvest.js +7 -5
  18. package/dist/esm/features/jserrors/aggregate/index.js +25 -12
  19. package/dist/esm/features/session_replay/aggregate/index.js +11 -5
  20. package/dist/esm/features/session_replay/constants.js +2 -1
  21. package/dist/esm/features/session_replay/instrument/index.js +16 -6
  22. package/dist/esm/features/session_replay/shared/recorder.js +6 -3
  23. package/dist/esm/features/session_replay/shared/utils.js +5 -3
  24. package/dist/esm/features/soft_navigations/aggregate/index.js +2 -2
  25. package/dist/esm/features/utils/instrument-base.js +11 -14
  26. package/dist/esm/features/utils/nr1-debugger.js +21 -0
  27. package/dist/esm/loaders/agent.js +4 -0
  28. package/dist/types/common/harvest/harvest.d.ts +1 -1
  29. package/dist/types/common/harvest/harvest.d.ts.map +1 -1
  30. package/dist/types/features/jserrors/aggregate/index.d.ts +0 -1
  31. package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
  32. package/dist/types/features/session_replay/aggregate/index.d.ts +0 -1
  33. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  34. package/dist/types/features/session_replay/constants.d.ts +1 -0
  35. package/dist/types/features/session_replay/constants.d.ts.map +1 -1
  36. package/dist/types/features/session_replay/instrument/index.d.ts +1 -0
  37. package/dist/types/features/session_replay/instrument/index.d.ts.map +1 -1
  38. package/dist/types/features/session_replay/shared/recorder.d.ts +1 -0
  39. package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
  40. package/dist/types/features/session_replay/shared/utils.d.ts +2 -2
  41. package/dist/types/features/session_replay/shared/utils.d.ts.map +1 -1
  42. package/dist/types/features/utils/instrument-base.d.ts.map +1 -1
  43. package/dist/types/features/utils/nr1-debugger.d.ts +2 -0
  44. package/dist/types/features/utils/nr1-debugger.d.ts.map +1 -0
  45. package/dist/types/loaders/agent.d.ts +5 -1
  46. package/dist/types/loaders/agent.d.ts.map +1 -1
  47. package/package.json +1 -1
  48. package/src/common/drain/__mocks__/drain.js +2 -0
  49. package/src/common/harvest/harvest.js +8 -6
  50. package/src/common/window/__mocks__/load.js +3 -0
  51. package/src/features/jserrors/aggregate/index.js +26 -10
  52. package/src/features/session_replay/aggregate/index.js +17 -7
  53. package/src/features/session_replay/constants.js +2 -1
  54. package/src/features/session_replay/instrument/index.js +16 -6
  55. package/src/features/session_replay/shared/__mocks__/utils.js +2 -0
  56. package/src/features/session_replay/shared/recorder.js +7 -4
  57. package/src/features/session_replay/shared/utils.js +5 -3
  58. package/src/features/soft_navigations/aggregate/index.js +2 -2
  59. package/src/features/utils/__mocks__/agent-session.js +1 -0
  60. package/src/features/utils/__mocks__/feature-base.js +11 -0
  61. package/src/features/utils/instrument-base.js +11 -14
  62. package/src/features/utils/nr1-debugger.js +22 -0
  63. package/src/loaders/agent.js +4 -0
@@ -9,9 +9,10 @@
9
9
  * It is not production ready, and is not intended to be imported or implemented in any build of the browser agent until
10
10
  * functionality is validated and a full user experience is curated.
11
11
  */
12
+ import { handle } from '../../../common/event-emitter/handle';
12
13
  import { DEFAULT_KEY, MODE, PREFIX } from '../../../common/session/constants';
13
14
  import { InstrumentBase } from '../../utils/instrument-base';
14
- import { FEATURE_NAME } from '../constants';
15
+ import { FEATURE_NAME, SR_EVENT_EMITTER_TYPES } from '../constants';
15
16
  import { isPreloadAllowed } from '../shared/utils';
16
17
  export class Instrument extends InstrumentBase {
17
18
  static featureName = FEATURE_NAME;
@@ -19,19 +20,28 @@ export class Instrument extends InstrumentBase {
19
20
  let auto = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
20
21
  super(agentIdentifier, aggregator, FEATURE_NAME, auto);
21
22
  let session;
23
+ this.replayRunning = false;
22
24
  try {
23
25
  session = JSON.parse(localStorage.getItem("".concat(PREFIX, "_").concat(DEFAULT_KEY)));
24
26
  } catch (err) {}
25
27
  if (this.#canPreloadRecorder(session)) {
26
- /** If this is preloaded, set up a buffer, if not, later when sampling we will set up a .on for live events */
27
- this.ee.on('err', e => {
28
- this.errorNoticed = true;
29
- if (this.featAggregate) this.featAggregate.handleError();
30
- });
31
28
  this.#startRecording(session?.sessionReplayMode);
32
29
  } else {
33
30
  this.importAggregator();
34
31
  }
32
+
33
+ /** If the recorder is running, we can pass error events on to the agg to help it switch to full mode later */
34
+ this.ee.on('err', e => {
35
+ if (this.replayRunning) {
36
+ this.errorNoticed = true;
37
+ handle(SR_EVENT_EMITTER_TYPES.ERROR_DURING_REPLAY, [e], undefined, this.featureName, this.ee);
38
+ }
39
+ });
40
+
41
+ /** Emitted by the recorder when it starts capturing data, used to determine if we should pass errors on to the agg */
42
+ this.ee.on(SR_EVENT_EMITTER_TYPES.REPLAY_RUNNING, isRunning => {
43
+ this.replayRunning = isRunning;
44
+ });
35
45
  }
36
46
 
37
47
  // At this point wherein session state exists already but we haven't init SessionEntity aka verify timers.
@@ -97,11 +97,10 @@ export class Recorder {
97
97
  collectFonts: collect_fonts,
98
98
  checkoutEveryNms: CHECKOUT_MS[this.parent.mode]
99
99
  });
100
- this.parent.ee.emit(SR_EVENT_EMITTER_TYPES.REPLAY_RUNNING, [true, this.parent.mode]);
101
100
  this.stopRecording = () => {
102
101
  this.recording = false;
103
102
  this.parent.ee.emit(SR_EVENT_EMITTER_TYPES.REPLAY_RUNNING, [false, this.parent.mode]);
104
- stop();
103
+ stop?.();
105
104
  };
106
105
  }
107
106
 
@@ -144,6 +143,10 @@ export class Recorder {
144
143
  if (!event) return;
145
144
  if (!this.parent.scheduler && this.#preloaded.length) this.currentBufferTarget = this.#preloaded[this.#preloaded.length - 1];else this.currentBufferTarget = this.#events;
146
145
  if (this.parent.blocked) return;
146
+ if (!this.notified) {
147
+ this.parent.ee.emit(SR_EVENT_EMITTER_TYPES.REPLAY_RUNNING, [true, this.parent.mode]);
148
+ this.notified = true;
149
+ }
147
150
  if (this.parent.timeKeeper?.ready && !event.__newrelic) {
148
151
  event.__newrelic = buildNRMetaNode(event.timestamp, this.parent.timeKeeper);
149
152
  event.timestamp = this.parent.timeKeeper.correctAbsoluteTimestamp(event.timestamp);
@@ -174,7 +177,7 @@ export class Recorder {
174
177
 
175
178
  // We are making an effort to try to keep payloads manageable for unloading. If they reach the unload limit before their interval,
176
179
  // it will send immediately. This often happens on the first snapshot, which can be significantly larger than the other payloads.
177
- if (payloadSize > IDEAL_PAYLOAD_SIZE && this.parent.mode !== MODE.ERROR) {
180
+ if ((event.type === RRWEB_EVENT_TYPES.FullSnapshot && this.currentBufferTarget.hasMeta || payloadSize > IDEAL_PAYLOAD_SIZE) && this.parent.mode === MODE.FULL) {
178
181
  // if we've made it to the ideal size of ~64kb before the interval timer, we should send early.
179
182
  if (this.parent.scheduler) {
180
183
  this.parent.scheduler.runHarvest();
@@ -1,10 +1,12 @@
1
1
  import { getConfigurationValue, originals } from '../../../common/config/config';
2
2
  import { isBrowserScope } from '../../../common/constants/runtime';
3
- export const enableSessionTracking = agentId => isBrowserScope && getConfigurationValue(agentId, 'privacy.cookies_enabled') === true;
3
+ export function enableSessionTracking(agentId) {
4
+ return isBrowserScope && getConfigurationValue(agentId, 'privacy.cookies_enabled') === true;
5
+ }
4
6
  function hasReplayPrerequisite(agentId) {
5
- return originals.MO &&
7
+ return !!originals.MO &&
6
8
  // Session Replay cannot work without Mutation Observer
7
- enableSessionTracking &&
9
+ enableSessionTracking(agentId) &&
8
10
  // requires session tracking to be running (hence "session" replay...)
9
11
  getConfigurationValue(agentId, 'session_trace.enabled') === true; // Session Replay as of now is tightly coupled with Session Trace in the UI
10
12
  }
@@ -82,7 +82,7 @@ export class Aggregate extends AggregateBase {
82
82
  if (!firstIxnStartTime) firstIxnStartTime = Math.floor(interaction.start);
83
83
  }
84
84
  const payload = "bel.7;".concat(serializedIxnList.join(';'));
85
- if (options.retry) this.interactionsAwaitingRetry.push(...this.interactionsToHarvest);
85
+ if (options.retry) this.interactionsAwaitingRetry = this.interactionsToHarvest;
86
86
  this.interactionsToHarvest = [];
87
87
  return {
88
88
  body: {
@@ -93,8 +93,8 @@ export class Aggregate extends AggregateBase {
93
93
  onHarvestFinished(result) {
94
94
  if (result.sent && result.retry && this.interactionsAwaitingRetry.length > 0) {
95
95
  this.interactionsToHarvest = [...this.interactionsAwaitingRetry, ...this.interactionsToHarvest];
96
- this.interactionsAwaitingRetry = [];
97
96
  }
97
+ this.interactionsAwaitingRetry = [];
98
98
  }
99
99
  startUIInteraction(eventName, startedAt, sourceElem) {
100
100
  // this is throttled by instrumentation so that it isn't excessively called
@@ -12,6 +12,7 @@ import { warn } from '../../common/util/console';
12
12
  import { FEATURE_NAMES } from '../../loaders/features/features';
13
13
  import { getConfigurationValue } from '../../common/config/config';
14
14
  import { canImportReplayAgg, enableSessionTracking } from '../session_replay/shared/utils';
15
+ import { single } from '../../common/util/invoke';
15
16
 
16
17
  /**
17
18
  * Base class for instrumenting a feature.
@@ -50,7 +51,15 @@ export class InstrumentBase extends FeatureBase {
50
51
  /** used in conjunction with newrelic.start() to defer harvesting in features */
51
52
  if (getConfigurationValue(this.agentIdentifier, "".concat(this.featureName, ".autoStart")) === false) this.auto = false;
52
53
  /** if the feature requires opt-in (!auto-start), it will get registered once the api has been called */
53
- if (this.auto) registerDrain(agentIdentifier, featureName);
54
+ if (this.auto) registerDrain(agentIdentifier, featureName);else {
55
+ this.ee.on("".concat(this.featureName, "-opt-in"), single(() => {
56
+ // register the feature to drain only once the API has been called, it will drain when importAggregator finishes for all the features
57
+ // called by the api in that cycle
58
+ registerDrain(this.agentIdentifier, this.featureName);
59
+ this.auto = true;
60
+ this.importAggregator();
61
+ }));
62
+ }
54
63
  }
55
64
 
56
65
  /**
@@ -61,19 +70,7 @@ export class InstrumentBase extends FeatureBase {
61
70
  */
62
71
  importAggregator() {
63
72
  let argsObjFromInstrument = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
64
- if (this.featAggregate) return;
65
- if (!this.auto) {
66
- // this feature requires an opt in...
67
- // wait for API to be called
68
- this.ee.on("".concat(this.featureName, "-opt-in"), () => {
69
- // register the feature to drain only once the API has been called, it will drain when importAggregator finishes for all the features
70
- // called by the api in that cycle
71
- registerDrain(this.agentIdentifier, this.featureName);
72
- this.auto = true;
73
- this.importAggregator();
74
- });
75
- return;
76
- }
73
+ if (this.featAggregate || !this.auto) return;
77
74
  let loadedSuccessfully;
78
75
  this.onAggregateImported = new Promise(resolve => {
79
76
  loadedSuccessfully = resolve;
@@ -0,0 +1,21 @@
1
+ import { gosCDN } from '../../common/window/nreum';
2
+ const debugId = 1;
3
+ const newrelic = gosCDN();
4
+ export function debugNR1(agentIdentifier, location, event) {
5
+ let otherprops = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
6
+ let debugName = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 'SR';
7
+ const api = agentIdentifier ? newrelic.initializedAgents[agentIdentifier].api.addPageAction : newrelic.addPageAction;
8
+ let url;
9
+ try {
10
+ const locURL = new URL(window.location);
11
+ url = locURL.pathname;
12
+ } catch (err) {}
13
+ api(debugName, {
14
+ debugId,
15
+ url,
16
+ location,
17
+ event,
18
+ now: performance.now(),
19
+ ...otherprops
20
+ });
21
+ }
@@ -21,6 +21,10 @@ import { globalScope } from '../common/constants/runtime';
21
21
  * sensitive to network load, this may result in smaller builds with slightly lower performance impact.
22
22
  */
23
23
  export class Agent extends AgentBase {
24
+ /**
25
+ * @param {Object} options Options to initialize agent with
26
+ * @param {string} [agentIdentifier] Optional identifier of agent
27
+ */
24
28
  constructor(options, agentIdentifier) {
25
29
  super(agentIdentifier);
26
30
  if (!globalScope) {
@@ -34,7 +34,7 @@ export class Harvest extends SharedContext {
34
34
  * value should not be relied upon because network calls will be made asynchronously.
35
35
  */
36
36
  _send({ endpoint, payload, opts, submitMethod, cbFinished, customUrl, raw, includeBaseParams }: NetworkSendSpec): boolean;
37
- baseQueryString(qs: any): string;
37
+ baseQueryString(qs: any, endpoint: any): string;
38
38
  /**
39
39
  * Calls and accumulates data from registered harvesting functions based on
40
40
  * the endpoint being harvested.
@@ -1 +1 @@
1
- {"version":3,"file":"harvest.d.ts","sourceRoot":"","sources":["../../../../src/common/harvest/harvest.js"],"names":[],"mappings":"AAsBA;;;;;;GAMG;AACH;IAII,0BAA2H;IAC3H,uBAAoD;IAEpD,YAAiB;IAGnB;;;;;OAKG;IACH,aAFW,eAAe,WAWzB;IAED;;;OAGG;IACH,YAFW,eAAe,WAMzB;IAED;;;OAGG;IACH,wBAFW,eAAe,WAMzB;IAED;;;;;;OAMG;IACH,gGAJW,eAAe,GACb,OAAO,CAsFnB;IAGD,iCAoBC;IAED;;;;;;;OAOG;IACH,wBALW,yBAAyB,WACzB,6BAA6B,GAE3B,cAAc,CA2B1B;IAED;;;;;;;OAOG;IACH,uBAHW,cAAc,GACZ,cAAc,CAuB1B;IAED;;;;;OAKG;IACH,aAHW,yBAAyB,YACzB,sBAAsB,QAQhC;CACF;8BAvPY,OAAO,YAAY,EAAE,eAAe;wCACpC,OAAO,YAAY,EAAE,yBAAyB;6BAC9C,OAAO,YAAY,EAAE,cAAc;qCACnC,OAAO,YAAY,EAAE,sBAAsB;4CAC3C,OAAO,YAAY,EAAE,6BAA6B;8BAbjC,2BAA2B;2BAF9B,mBAAmB"}
1
+ {"version":3,"file":"harvest.d.ts","sourceRoot":"","sources":["../../../../src/common/harvest/harvest.js"],"names":[],"mappings":"AAsBA;;;;;;GAMG;AACH;IAII,0BAA2H;IAC3H,uBAAoD;IAEpD,YAAiB;IAGnB;;;;;OAKG;IACH,aAFW,eAAe,WAWzB;IAED;;;OAGG;IACH,YAFW,eAAe,WAMzB;IAED;;;OAGG;IACH,wBAFW,eAAe,WAMzB;IAED;;;;;;OAMG;IACH,gGAJW,eAAe,GACb,OAAO,CAsFnB;IAGD,gDAsBC;IAED;;;;;;;OAOG;IACH,wBALW,yBAAyB,WACzB,6BAA6B,GAE3B,cAAc,CA2B1B;IAED;;;;;;;OAOG;IACH,uBAHW,cAAc,GACZ,cAAc,CAuB1B;IAED;;;;;OAKG;IACH,aAHW,yBAAyB,YACzB,sBAAsB,QAQhC;CACF;8BAzPY,OAAO,YAAY,EAAE,eAAe;wCACpC,OAAO,YAAY,EAAE,yBAAyB;6BAC9C,OAAO,YAAY,EAAE,cAAc;qCACnC,OAAO,YAAY,EAAE,sBAAsB;4CAC3C,OAAO,YAAY,EAAE,6BAA6B;8BAbjC,2BAA2B;2BAF9B,mBAAmB"}
@@ -10,7 +10,6 @@ export class Aggregate extends AggregateBase {
10
10
  bufferedErrorsUnderSpa: {};
11
11
  currentBody: {} | null | undefined;
12
12
  errorOnPage: boolean;
13
- replayAborted: boolean;
14
13
  onHarvestStarted(options: any): {
15
14
  body: {} | null;
16
15
  qs: {};
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/jserrors/aggregate/index.js"],"names":[],"mappings":"AAyBA;;GAEG;AAEH;IACE,2BAAiC;IACjC,mDAmCC;IAhCC,kBAAuB;IACvB,eAAoB;IACpB,qBAA0B;IAC1B,2BAAgC;IAChC,mCAA4B;IAC5B,qBAAwB;IACxB,uBAA0B;IA4B5B;;;MA2BC;IAED,qCAWC;IAED,8BAEC;IAED,oEAMC;IAED;;;;;;OAMG;IACH,qCAHW,SAAS,GACP,MAAM,CAgBlB;IAED,4FAsFC;IA4BD,yDA6BC;IAED,qFAOC;;CACF;wBAhRY,OAAO,0BAA0B,EAAE,SAAS;8BAN3B,4BAA4B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/jserrors/aggregate/index.js"],"names":[],"mappings":"AAyBA;;GAEG;AAEH;IACE,2BAAiC;IACjC,mDAgCC;IA7BC,kBAAuB;IACvB,eAAoB;IACpB,qBAA0B;IAC1B,2BAAgC;IAChC,mCAA4B;IAC5B,qBAAwB;IA0B1B;;;MAuBC;IAED,qCAWC;IAED,8BAEC;IAED,oEAMC;IAED;;;;;;OAMG;IACH,qCAHW,SAAS,GACP,MAAM,CAgBlB;IAED,4FAsFC;IA4BD,yDA6BC;IAED,qFAOC;;CAwBF;wBAhSY,OAAO,0BAA0B,EAAE,SAAS;8BAN3B,4BAA4B"}
@@ -15,7 +15,6 @@ export class Aggregate extends AggregateBase {
15
15
  /** set at BCS response, stored in runtime */
16
16
  timeKeeper: any;
17
17
  recorder: any;
18
- preloaded: boolean;
19
18
  errorNoticed: any;
20
19
  scheduler: HarvestScheduler;
21
20
  handleError(e: any): void;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_replay/aggregate/index.js"],"names":[],"mappings":"AAgCA;IACE,2BAAiC;IAIjC,8DAsGC;IAzGD,aAAe;IAKb,8GAA8G;IAC9G,wBAAgH;IAChH,iFAAiF;IACjF,qBAAwB;IAGxB,2CAA2C;IAC3C,sDAAwB;IACxB,6CAA6C;IAC7C,gDAAmB;IAEnB,0BAA0B;IAC1B,kBAAqB;IACrB,6CAA6C;IAC7C,gBAA2B;IAE3B,cAA8B;IAC9B,mBAAgC;IAChC,kBAA+C;IA4B/C,4BAKQ;IAmDV,0BAMC;IAED,qBAWC;IAED;;;;;;;OAOG;IACH,iCALW,OAAO,cACP,OAAO,iBACP,OAAO,GACL,IAAI,CA8DhB;IAED,2BASC;IAED;;;;;;;;;;;;oBAiDC;IAED,sCAIC;IAED;;;;;;;;;;MAoEC;IAED,qCAOC;IAED;;;;OAIG;IACH,mCAKC;IAED,yDAAyD;IACzD,yBAUC;IAED,yCAGC;CACF;8BAxY6B,4BAA4B;iCAHzB,2CAA2C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_replay/aggregate/index.js"],"names":[],"mappings":"AAgCA;IACE,2BAAiC;IAIjC,8DAiHC;IApHD,aAAe;IAKb,8GAA8G;IAC9G,wBAAgH;IAChH,iFAAiF;IACjF,qBAAwB;IAGxB,2CAA2C;IAC3C,sDAAwB;IACxB,6CAA6C;IAC7C,gDAAmB;IAEnB,0BAA0B;IAC1B,kBAAqB;IACrB,6CAA6C;IAC7C,gBAA2B;IAE3B,cAA8B;IAC9B,kBAA+C;IAoC/C,4BAKQ;IAuDV,0BAMC;IAED,qBAWC;IAED;;;;;;;OAOG;IACH,iCALW,OAAO,cACP,OAAO,iBACP,OAAO,GACL,IAAI,CA6DhB;IAED,2BASC;IAED;;;;;;;;;;;;oBAiDC;IAED,sCAIC;IAED;;;;;;;;;;MAoEC;IAED,qCAOC;IAED;;;;OAIG;IACH,mCAKC;IAED,yDAAyD;IACzD,yBAUC;IAED,yCAGC;CACF;8BAlZ6B,4BAA4B;iCAHzB,2CAA2C"}
@@ -3,6 +3,7 @@ export namespace SR_EVENT_EMITTER_TYPES {
3
3
  let RECORD: string;
4
4
  let PAUSE: string;
5
5
  let REPLAY_RUNNING: string;
6
+ let ERROR_DURING_REPLAY: string;
6
7
  }
7
8
  export const AVG_COMPRESSION: 0.12;
8
9
  export namespace RRWEB_EVENT_TYPES {
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../../src/features/session_replay/constants.js"],"names":[],"mappings":"AAGA,kCAAuD;;;;;;AAQvD,mCAAmC;;;;;;;;;AASnC,uCAAuC;AACvC,uCAAuC;AACvC,iCAAiC;AACjC,uCAAuC;AACvC,2GAA2G;AAC3G;;EAAsF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BtF,0CAA0C;AAC1C,uCAAuC"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../../src/features/session_replay/constants.js"],"names":[],"mappings":"AAGA,kCAAuD;;;;;;;AASvD,mCAAmC;;;;;;;;;AASnC,uCAAuC;AACvC,uCAAuC;AACvC,iCAAiC;AACjC,uCAAuC;AACvC,2GAA2G;AAC3G;;EAAsF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BtF,0CAA0C;AAC1C,uCAAuC"}
@@ -1,6 +1,7 @@
1
1
  export class Instrument extends InstrumentBase {
2
2
  static featureName: string;
3
3
  constructor(agentIdentifier: any, aggregator: any, auto?: boolean);
4
+ replayRunning: boolean;
4
5
  errorNoticed: boolean;
5
6
  recorder: import("../shared/recorder").Recorder | undefined;
6
7
  #private;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_replay/instrument/index.js"],"names":[],"mappings":"AAgBA;IACE,2BAAiC;IACjC,mEAiBC;IAPK,sBAAwB;IAwB5B,4DAA0F;;CAK7F;+BA7C8B,6BAA6B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_replay/instrument/index.js"],"names":[],"mappings":"AAiBA;IACE,2BAAiC;IACjC,mEA0BC;IAvBC,uBAA0B;IActB,sBAAwB;IA0B5B,4DAA0F;;CAK7F;+BAtD8B,6BAA6B"}
@@ -37,6 +37,7 @@ export class Recorder {
37
37
  audit(event: any, isCheckout: any): void;
38
38
  /** Store a payload in the buffer (this.#events). This should be the callback to the recording lib noticing a mutation */
39
39
  store(event: any, isCheckout: any): void;
40
+ notified: boolean | undefined;
40
41
  /** force the recording lib to take a full DOM snapshot. This needs to occur in certain cases, like visibility changes */
41
42
  takeFullSnapshot(): void;
42
43
  clearTimestamps(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"recorder.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_replay/shared/recorder.js"],"names":[],"mappings":"AAYA;IAUE,yBAkBC;IAdC,iEAAiE;IACjE,mBAAsB;IACtB,6DAA6D;IAC7D,oCAAuC;IACvC,kIAAkI;IAClI,kBAAqB;IACrB,sDAAsD;IACtD,YAAoB;IACpB,oEAAoE;IACpE,6BAAqH;IACrH,0FAA0F;IAC1F,eAA6C;IAC7C,uIAAuI;IACvI,0BAAyE;IAG3E;;;;;;;;;MAYC;IAED,mFAAmF;IACnF,oBAKC;IAED,qDAAqD;IACrD,uBAiCC;IAED;;;;;OAKG;IACH,yCA0BC;IAED,0HAA0H;IAC1H,yCAgDC;IAED,0HAA0H;IAC1H,yBAOC;IAED,wBAEC;IAED,gCAAgC;IAChC,uCAGC;IAED;;;SAGK;IACL,oCAGC;;CACF;+BAjN8B,mBAAmB"}
1
+ {"version":3,"file":"recorder.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_replay/shared/recorder.js"],"names":[],"mappings":"AAYA;IAUE,yBAkBC;IAdC,iEAAiE;IACjE,mBAAsB;IACtB,6DAA6D;IAC7D,oCAAuC;IACvC,kIAAkI;IAClI,kBAAqB;IACrB,sDAAsD;IACtD,YAAoB;IACpB,oEAAoE;IACpE,6BAAqH;IACrH,0FAA0F;IAC1F,eAA6C;IAC7C,uIAAuI;IACvI,0BAAyE;IAG3E;;;;;;;;;MAYC;IAED,mFAAmF;IACnF,oBAKC;IAED,qDAAqD;IACrD,uBA+BC;IAED;;;;;OAKG;IACH,yCA0BC;IAED,0HAA0H;IAC1H,yCAqDC;IA3CG,8BAAoB;IA6CxB,0HAA0H;IAC1H,yBAOC;IAED,wBAEC;IAED,gCAAgC;IAChC,uCAGC;IAED;;;SAGK;IACL,oCAGC;;CACF;+BApN8B,mBAAmB"}
@@ -1,4 +1,5 @@
1
- export function isPreloadAllowed(agentId: any): any;
1
+ export function enableSessionTracking(agentId: any): boolean;
2
+ export function isPreloadAllowed(agentId: any): boolean;
2
3
  export function canImportReplayAgg(agentId: any, sessionMgr: any): boolean;
3
4
  export function buildNRMetaNode(timestamp: any, timeKeeper: any): {
4
5
  originalTimestamp: any;
@@ -8,5 +9,4 @@ export function buildNRMetaNode(timestamp: any, timeKeeper: any): {
8
9
  timeKeeperCorrectedOriginTime: any;
9
10
  timeKeeperDiff: number;
10
11
  };
11
- export function enableSessionTracking(agentId: any): boolean;
12
12
  //# 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":"AAWA,oDAEC;AAED,2EAGC;AAED;;;;;;;EAUC;AA3BM,6DAA+H"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_replay/shared/utils.js"],"names":[],"mappings":"AAGA,6DAEC;AAQD,wDAEC;AAED,2EAGC;AAED;;;;;;;EAUC"}
@@ -1 +1 @@
1
- {"version":3,"file":"instrument-base.d.ts","sourceRoot":"","sources":["../../../../src/features/utils/instrument-base.js"],"names":[],"mappings":"AAeA;;;GAGG;AACH;IACE;;;;;;;;OAQG;IACH,6BAPW,MAAM,cACN,OAAO,mCAAmC,EAAE,UAAU,eACtD,MAAM,8BA4BhB;IArBC,cAAgB;IAEhB,8IAA8I;IAC9I,cADW,WAAW,SAAS,CACF;IAE7B;;;MAGE;IACF,eAHU,OAAO,kBAAkB,EAAE,aAAa,CAGpB;IAE9B;;;MAGE;IACF,kCAAoC;IAQtC;;;;;OAKG;IACH,mEA6DC;;CAYF;4BA9H2B,gBAAgB"}
1
+ {"version":3,"file":"instrument-base.d.ts","sourceRoot":"","sources":["../../../../src/features/utils/instrument-base.js"],"names":[],"mappings":"AAgBA;;;GAGG;AACH;IACE;;;;;;;;OAQG;IACH,6BAPW,MAAM,cACN,OAAO,mCAAmC,EAAE,UAAU,eACtD,MAAM,8BAqChB;IA9BC,cAAgB;IAEhB,8IAA8I;IAC9I,cADW,WAAW,SAAS,CACF;IAE7B;;;MAGE;IACF,eAHU,OAAO,kBAAkB,EAAE,aAAa,CAGpB;IAE9B;;;MAGE;IACF,kCAAoC;IAiBtC;;;;;OAKG;IACH,mEAgDC;;CAYF;4BA3H2B,gBAAgB"}
@@ -0,0 +1,2 @@
1
+ export function debugNR1(agentIdentifier: any, location: any, event: any, otherprops?: {}, debugName?: string): void;
2
+ //# sourceMappingURL=nr1-debugger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nr1-debugger.d.ts","sourceRoot":"","sources":["../../../../src/features/utils/nr1-debugger.js"],"names":[],"mappings":"AAIA,qHAiBC"}
@@ -3,7 +3,11 @@
3
3
  * sensitive to network load, this may result in smaller builds with slightly lower performance impact.
4
4
  */
5
5
  export class Agent extends AgentBase {
6
- constructor(options: any, agentIdentifier: any);
6
+ /**
7
+ * @param {Object} options Options to initialize agent with
8
+ * @param {string} [agentIdentifier] Optional identifier of agent
9
+ */
10
+ constructor(options: Object, agentIdentifier?: string | undefined);
7
11
  sharedAggregator: Aggregator | undefined;
8
12
  features: {} | undefined;
9
13
  desiredFeatures: Set<any> | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../../src/loaders/agent.js"],"names":[],"mappings":"AAkBA;;;GAGG;AACH;IACE,gDAwBC;IAdC,yCAAiF;IACjF,yBAAkB;IAGlB,sCAAsD;IAMtD,uCAA6G;IAM/G;;;;;MAOC;IAED,yBAiCC;CACF;0BAxFyB,cAAc;2BAQb,gCAAgC"}
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../../src/loaders/agent.js"],"names":[],"mappings":"AAkBA;;;GAGG;AACH;IACE;;;OAGG;IACH,qBAHW,MAAM,wCA2BhB;IAdC,yCAAiF;IACjF,yBAAkB;IAGlB,sCAAsD;IAMtD,uCAA6G;IAM/G;;;;;MAOC;IAED,yBAiCC;CACF;0BA5FyB,cAAc;2BAQb,gCAAgC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newrelic/browser-agent",
3
- "version": "1.256.1",
3
+ "version": "1.257.0",
4
4
  "private": false,
5
5
  "author": "New Relic Browser Agent Team <browser-agent@newrelic.com>",
6
6
  "description": "New Relic Browser Agent",
@@ -0,0 +1,2 @@
1
+ export const drain = jest.fn()
2
+ export const registerDrain = jest.fn()
@@ -103,7 +103,7 @@ export class Harvest extends SharedContext {
103
103
  if (customUrl) url = customUrl
104
104
  if (raw) url = `${protocol}://${perceviedBeacon}/${endpoint}`
105
105
 
106
- const baseParams = !raw && includeBaseParams ? this.baseQueryString(qs) : ''
106
+ const baseParams = !raw && includeBaseParams ? this.baseQueryString(qs, endpoint) : ''
107
107
  let payloadParams = encodeObj(qs, agentRuntime.maxBytes)
108
108
  if (!submitMethod) {
109
109
  submitMethod = submitData.getSubmitMethod({ isFinalHarvest: opts.unload })
@@ -167,14 +167,15 @@ export class Harvest extends SharedContext {
167
167
  }
168
168
 
169
169
  // The stuff that gets sent every time.
170
- baseQueryString (qs) {
170
+ baseQueryString (qs, endpoint) {
171
171
  const runtime = getRuntime(this.sharedContext.agentIdentifier)
172
172
  const info = getInfo(this.sharedContext.agentIdentifier)
173
173
 
174
174
  const location = cleanURL(getLocation())
175
175
  const ref = this.obfuscator.shouldObfuscate() ? this.obfuscator.obfuscateString(location) : location
176
+ const hr = runtime?.session?.state.sessionReplayMode === 1 && endpoint !== 'jserrors'
176
177
 
177
- return ([
178
+ const qps = [
178
179
  'a=' + info.applicationID,
179
180
  encodeParam('sa', (info.sa ? '' + info.sa : '')),
180
181
  encodeParam('v', VERSION),
@@ -184,9 +185,10 @@ export class Harvest extends SharedContext {
184
185
  '&ck=0', // ck param DEPRECATED - still expected by backend
185
186
  '&s=' + (runtime.session?.state.value || '0'), // the 0 id encaps all untrackable and default traffic
186
187
  encodeParam('ref', ref),
187
- encodeParam('ptid', (runtime.ptid ? '' + runtime.ptid : '')),
188
- encodeParam('hr', (runtime?.session?.state.sessionReplayMode === 1 ? '1' : '0'), qs) // hasReplay
189
- ].join(''))
188
+ encodeParam('ptid', (runtime.ptid ? '' + runtime.ptid : ''))
189
+ ]
190
+ if (hr) qps.push(encodeParam('hr', '1', qs))
191
+ return qps.join('')
190
192
  }
191
193
 
192
194
  /**
@@ -0,0 +1,3 @@
1
+ export const checkState = jest.fn()
2
+ export const onWindowLoad = jest.fn()
3
+ export const onDOMContentLoaded = jest.fn()
@@ -38,13 +38,10 @@ export class Aggregate extends AggregateBase {
38
38
  this.bufferedErrorsUnderSpa = {}
39
39
  this.currentBody = undefined
40
40
  this.errorOnPage = false
41
- this.replayAborted = false
42
41
 
43
42
  // this will need to change to match whatever ee we use in the instrument
44
43
  this.ee.on('interactionDone', (interaction, wasSaved) => this.onInteractionDone(interaction, wasSaved))
45
44
 
46
- this.ee.on('REPLAY_ABORTED', () => { this.replayAborted = true })
47
-
48
45
  register('err', (...args) => this.storeError(...args), this.featureName, this.ee)
49
46
  register('ierr', (...args) => this.storeError(...args), this.featureName, this.ee)
50
47
  register('softNavFlush', (interactionId, wasFinished, softNavAttrs) =>
@@ -82,11 +79,7 @@ export class Aggregate extends AggregateBase {
82
79
  }
83
80
 
84
81
  if (body && body.err && body.err.length) {
85
- if (this.replayAborted) {
86
- body.err.forEach((e) => {
87
- delete e.params?.hasReplay
88
- })
89
- }
82
+ this.#runCrossFeatureChecks(body.err)
90
83
  if (!this.errorOnPage) {
91
84
  payload.qs.pve = '1'
92
85
  this.errorOnPage = true
@@ -172,13 +165,14 @@ export class Aggregate extends AggregateBase {
172
165
  // Do not modify the name ('errorGroup') of params without DEM approval!
173
166
  if (filterOutput?.group) params.errorGroup = filterOutput.group
174
167
 
168
+ if (hasReplay) params.hasReplay = hasReplay
175
169
  /**
176
170
  * The bucketHash is different from the params.stackHash because the params.stackHash is based on the canonicalized
177
171
  * stack trace and is used downstream in NR1 to attempt to group the same errors across different browsers. However,
178
172
  * the canonical stack trace excludes items like the column number increasing the hit-rate of different errors potentially
179
173
  * bucketing and ultimately resulting in the loss of data in NR1.
180
174
  */
181
- var bucketHash = stringHashCode(`${stackInfo.name}_${stackInfo.message}_${stackInfo.stackString}`)
175
+ var bucketHash = stringHashCode(`${stackInfo.name}_${stackInfo.message}_${stackInfo.stackString}_${params.hasReplay ? 1 : 0}`)
182
176
 
183
177
  if (!this.stackReported[bucketHash]) {
184
178
  this.stackReported[bucketHash] = true
@@ -199,7 +193,6 @@ export class Aggregate extends AggregateBase {
199
193
  this.pageviewReported[bucketHash] = true
200
194
  }
201
195
 
202
- if (hasReplay && !this.replayAborted) params.hasReplay = hasReplay
203
196
  params.firstOccurrenceTimestamp = this.observedAt[bucketHash]
204
197
  params.timestamp = this.observedAt[bucketHash]
205
198
 
@@ -296,4 +289,27 @@ export class Aggregate extends AggregateBase {
296
289
  )
297
290
  delete this.bufferedErrorsUnderSpa[interactionId] // wipe the list of jserrors so they aren't duplicated by another call to the same id
298
291
  }
292
+
293
+ /**
294
+ * Dispatches a cross-feature communication event to allow other
295
+ * features to provide flags and data that can be used to mutation
296
+ * to the payload and to allow features to know about a feature
297
+ * harvest happening.
298
+ * @param {any[]} errors Array of errors from the payload body
299
+ */
300
+ #runCrossFeatureChecks (errors) {
301
+ const errorHashes = errors.map(error => error.params.stackHash)
302
+ const crossFeatureData = {
303
+ errorHashes
304
+ }
305
+ this.ee.emit(`cfc.${this.featureName}`, [crossFeatureData])
306
+
307
+ let hasReplayFlag = errors.find(err => err.params.hasReplay)
308
+ if (hasReplayFlag && !crossFeatureData.hasReplay) {
309
+ // Some errors have `hasReplay` and a replay is not being recorded
310
+ errors.forEach(error => {
311
+ delete error.params.hasReplay
312
+ })
313
+ }
314
+ }
299
315
  }
@@ -54,11 +54,18 @@ export class Aggregate extends AggregateBase {
54
54
  this.timeKeeper = undefined
55
55
 
56
56
  this.recorder = args?.recorder
57
- this.preloaded = !!this.recorder
58
57
  this.errorNoticed = args?.errorNoticed || false
59
58
 
60
59
  handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/Enabled'], undefined, FEATURE_NAMES.metrics, this.ee)
61
60
 
61
+ this.ee.on(`cfc.${FEATURE_NAMES.jserrors}`, (crossFeatureData) => {
62
+ crossFeatureData.hasReplay = !!(this.scheduler?.started &&
63
+ this.recorder &&
64
+ this.mode === MODE.FULL &&
65
+ !this.blocked &&
66
+ this.entitled)
67
+ })
68
+
62
69
  // The SessionEntity class can emit a message indicating the session was cleared and reset (expiry, inactivity). This feature must abort and never resume if that occurs.
63
70
  this.ee.on(SESSION_EVENTS.RESET, () => {
64
71
  this.abort(ABORT_REASONS.RESET)
@@ -104,6 +111,10 @@ export class Aggregate extends AggregateBase {
104
111
  this.forceStop(this.mode !== MODE.ERROR)
105
112
  }, this.featureName, this.ee)
106
113
 
114
+ registerHandler(SR_EVENT_EMITTER_TYPES.ERROR_DURING_REPLAY, e => {
115
+ this.handleError(e)
116
+ }, this.featureName, this.ee)
117
+
107
118
  const { error_sampling_rate, sampling_rate, autoStart, block_selector, mask_text_selector, mask_all_inputs, inline_stylesheet, inline_images, collect_fonts } = getConfigurationValue(this.agentIdentifier, 'session_replay')
108
119
 
109
120
  this.waitForFlags(['sr']).then(([flagOn]) => {
@@ -148,15 +159,15 @@ export class Aggregate extends AggregateBase {
148
159
  }
149
160
 
150
161
  switchToFull () {
162
+ if (!this.entitled || this.blocked) return
151
163
  this.mode = MODE.FULL
152
164
  // if the error was noticed AFTER the recorder was already imported....
153
165
  if (this.recorder && this.initialized) {
154
- this.recorder.stopRecording()
155
- this.recorder.startRecording()
156
-
166
+ if (!this.recorder.recording) this.recorder.startRecording()
157
167
  this.scheduler.startTimer(this.harvestTimeSeconds)
158
-
159
168
  this.syncWithSessionManager({ sessionReplayMode: this.mode })
169
+ } else {
170
+ this.initializeRecording(false, true, true)
160
171
  }
161
172
  }
162
173
 
@@ -213,7 +224,6 @@ export class Aggregate extends AggregateBase {
213
224
 
214
225
  // If an error was noticed before the mode could be set (like in the early lifecycle of the page), immediately set to FULL mode
215
226
  if (this.mode === MODE.ERROR && this.errorNoticed) this.mode = MODE.FULL
216
- if (!this.preloaded) this.ee.on('err', e => this.handleError(e))
217
227
 
218
228
  // FULL mode records AND reports from the beginning, while ERROR mode only records (but does not report).
219
229
  // ERROR mode will do this until an error is thrown, and then switch into FULL mode.
@@ -293,7 +303,7 @@ export class Aggregate extends AggregateBase {
293
303
  }
294
304
 
295
305
  getCorrectedTimestamp (node) {
296
- if (!node.timestamp) return
306
+ if (!node?.timestamp) return
297
307
  if (node.__newrelic) return node.timestamp
298
308
  return this.timeKeeper.correctAbsoluteTimestamp(node.timestamp)
299
309
  }
@@ -6,7 +6,8 @@ export const FEATURE_NAME = FEATURE_NAMES.sessionReplay
6
6
  export const SR_EVENT_EMITTER_TYPES = {
7
7
  RECORD: 'recordReplay',
8
8
  PAUSE: 'pauseReplay',
9
- REPLAY_RUNNING: 'replayRunning'
9
+ REPLAY_RUNNING: 'replayRunning',
10
+ ERROR_DURING_REPLAY: 'errorDuringReplay'
10
11
  }
11
12
 
12
13
  export const AVG_COMPRESSION = 0.12
@@ -9,9 +9,10 @@
9
9
  * It is not production ready, and is not intended to be imported or implemented in any build of the browser agent until
10
10
  * functionality is validated and a full user experience is curated.
11
11
  */
12
+ import { handle } from '../../../common/event-emitter/handle'
12
13
  import { DEFAULT_KEY, MODE, PREFIX } from '../../../common/session/constants'
13
14
  import { InstrumentBase } from '../../utils/instrument-base'
14
- import { FEATURE_NAME } from '../constants'
15
+ import { FEATURE_NAME, SR_EVENT_EMITTER_TYPES } from '../constants'
15
16
  import { isPreloadAllowed } from '../shared/utils'
16
17
 
17
18
  export class Instrument extends InstrumentBase {
@@ -19,20 +20,29 @@ export class Instrument extends InstrumentBase {
19
20
  constructor (agentIdentifier, aggregator, auto = true) {
20
21
  super(agentIdentifier, aggregator, FEATURE_NAME, auto)
21
22
  let session
23
+ this.replayRunning = false
22
24
  try {
23
25
  session = JSON.parse(localStorage.getItem(`${PREFIX}_${DEFAULT_KEY}`))
24
26
  } catch (err) { }
25
27
 
26
28
  if (this.#canPreloadRecorder(session)) {
27
- /** If this is preloaded, set up a buffer, if not, later when sampling we will set up a .on for live events */
28
- this.ee.on('err', (e) => {
29
- this.errorNoticed = true
30
- if (this.featAggregate) this.featAggregate.handleError()
31
- })
32
29
  this.#startRecording(session?.sessionReplayMode)
33
30
  } else {
34
31
  this.importAggregator()
35
32
  }
33
+
34
+ /** If the recorder is running, we can pass error events on to the agg to help it switch to full mode later */
35
+ this.ee.on('err', (e) => {
36
+ if (this.replayRunning) {
37
+ this.errorNoticed = true
38
+ handle(SR_EVENT_EMITTER_TYPES.ERROR_DURING_REPLAY, [e], undefined, this.featureName, this.ee)
39
+ }
40
+ })
41
+
42
+ /** Emitted by the recorder when it starts capturing data, used to determine if we should pass errors on to the agg */
43
+ this.ee.on(SR_EVENT_EMITTER_TYPES.REPLAY_RUNNING, (isRunning) => {
44
+ this.replayRunning = isRunning
45
+ })
36
46
  }
37
47
 
38
48
  // At this point wherein session state exists already but we haven't init SessionEntity aka verify timers.
@@ -0,0 +1,2 @@
1
+ export const enableSessionTracking = jest.fn()
2
+ export const canImportReplayAgg = jest.fn()