@newrelic/browser-agent 1.244.0 → 1.246.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 (104) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/cjs/cdn/polyfills.js +5 -1
  3. package/dist/cjs/common/config/state/configurable.js +1 -1
  4. package/dist/cjs/common/config/state/init.js +1 -0
  5. package/dist/cjs/common/constants/env.cdn.js +1 -1
  6. package/dist/cjs/common/constants/env.npm.js +1 -1
  7. package/dist/cjs/common/session/session-entity.js +3 -2
  8. package/dist/cjs/common/url/parse-url.js +21 -44
  9. package/dist/cjs/common/util/type-check.js +14 -0
  10. package/dist/cjs/common/vitals/first-input-delay.js +1 -2
  11. package/dist/cjs/common/vitals/largest-contentful-paint.js +1 -2
  12. package/dist/cjs/common/vitals/vital-metric.js +2 -12
  13. package/dist/cjs/features/ajax/aggregate/gql.js +94 -0
  14. package/dist/cjs/features/ajax/aggregate/index.js +13 -1
  15. package/dist/cjs/features/ajax/instrument/index.js +2 -0
  16. package/dist/cjs/features/jserrors/aggregate/index.js +1 -1
  17. package/dist/cjs/features/page_view_event/aggregate/index.js +2 -0
  18. package/dist/cjs/features/page_view_timing/aggregate/index.js +10 -1
  19. package/dist/cjs/features/session_replay/aggregate/index.js +6 -6
  20. package/dist/cjs/features/session_trace/aggregate/index.js +14 -3
  21. package/dist/cjs/features/spa/aggregate/index.js +5 -3
  22. package/dist/cjs/features/spa/aggregate/serializer.js +7 -0
  23. package/dist/cjs/features/utils/instrument-base.js +1 -1
  24. package/dist/cjs/index.js +0 -7
  25. package/dist/cjs/loaders/api/api.js +2 -2
  26. package/dist/esm/cdn/polyfills.js +5 -1
  27. package/dist/esm/common/config/state/configurable.js +1 -1
  28. package/dist/esm/common/config/state/init.js +1 -0
  29. package/dist/esm/common/constants/env.cdn.js +1 -1
  30. package/dist/esm/common/constants/env.npm.js +1 -1
  31. package/dist/esm/common/session/session-entity.js +3 -2
  32. package/dist/esm/common/url/parse-url.js +22 -45
  33. package/dist/esm/common/util/type-check.js +8 -0
  34. package/dist/esm/common/vitals/first-input-delay.js +1 -2
  35. package/dist/esm/common/vitals/largest-contentful-paint.js +1 -2
  36. package/dist/esm/common/vitals/vital-metric.js +1 -11
  37. package/dist/esm/features/ajax/aggregate/gql.js +89 -0
  38. package/dist/esm/features/ajax/aggregate/index.js +13 -1
  39. package/dist/esm/features/ajax/instrument/index.js +2 -0
  40. package/dist/esm/features/jserrors/aggregate/index.js +1 -1
  41. package/dist/esm/features/page_view_event/aggregate/index.js +2 -0
  42. package/dist/esm/features/page_view_timing/aggregate/index.js +9 -0
  43. package/dist/esm/features/session_replay/aggregate/index.js +6 -6
  44. package/dist/esm/features/session_trace/aggregate/index.js +14 -3
  45. package/dist/esm/features/spa/aggregate/index.js +5 -3
  46. package/dist/esm/features/spa/aggregate/serializer.js +7 -0
  47. package/dist/esm/features/utils/instrument-base.js +1 -1
  48. package/dist/esm/index.js +0 -1
  49. package/dist/esm/loaders/api/api.js +2 -2
  50. package/dist/types/common/config/state/configurable.d.ts.map +1 -1
  51. package/dist/types/common/config/state/init.d.ts.map +1 -1
  52. package/dist/types/common/session/session-entity.d.ts.map +1 -1
  53. package/dist/types/common/url/parse-url.d.ts +12 -1
  54. package/dist/types/common/url/parse-url.d.ts.map +1 -1
  55. package/dist/types/common/util/type-check.d.ts +7 -0
  56. package/dist/types/common/util/type-check.d.ts.map +1 -0
  57. package/dist/types/common/vitals/vital-metric.d.ts +1 -2
  58. package/dist/types/common/vitals/vital-metric.d.ts.map +1 -1
  59. package/dist/types/features/ajax/aggregate/gql.d.ts +29 -0
  60. package/dist/types/features/ajax/aggregate/gql.d.ts.map +1 -0
  61. package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
  62. package/dist/types/features/page_view_event/aggregate/index.d.ts.map +1 -1
  63. package/dist/types/features/page_view_timing/aggregate/index.d.ts.map +1 -1
  64. package/dist/types/features/session_trace/aggregate/index.d.ts +1 -0
  65. package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
  66. package/dist/types/features/spa/aggregate/index.d.ts +1 -0
  67. package/dist/types/features/spa/aggregate/index.d.ts.map +1 -1
  68. package/dist/types/features/spa/aggregate/serializer.d.ts.map +1 -1
  69. package/dist/types/index.d.ts +0 -1
  70. package/dist/types/loaders/configure/public-path.npm.d.ts.map +1 -1
  71. package/package.json +1 -9
  72. package/src/cdn/polyfills.js +4 -0
  73. package/src/common/config/state/configurable.js +2 -1
  74. package/src/common/config/state/init.js +1 -0
  75. package/src/common/session/session-entity.js +3 -2
  76. package/src/common/url/parse-url.js +22 -50
  77. package/src/common/util/type-check.js +8 -0
  78. package/src/common/vitals/first-input-delay.js +1 -2
  79. package/src/common/vitals/largest-contentful-paint.js +1 -2
  80. package/src/common/vitals/vital-metric.js +2 -12
  81. package/src/features/ajax/aggregate/gql.js +95 -0
  82. package/src/features/ajax/aggregate/index.js +11 -1
  83. package/src/features/ajax/instrument/index.js +3 -0
  84. package/src/features/jserrors/aggregate/index.js +1 -1
  85. package/src/features/page_view_event/aggregate/index.js +2 -0
  86. package/src/features/page_view_timing/aggregate/index.js +11 -0
  87. package/src/features/session_replay/aggregate/index.js +6 -6
  88. package/src/features/session_trace/aggregate/index.js +10 -2
  89. package/src/features/spa/aggregate/index.js +5 -3
  90. package/src/features/spa/aggregate/serializer.js +7 -1
  91. package/src/features/utils/instrument-base.js +1 -1
  92. package/src/index.js +0 -1
  93. package/src/loaders/api/api.js +2 -2
  94. package/src/loaders/configure/public-path.npm.js +0 -1
  95. package/dist/cjs/cdn/worker.js +0 -16
  96. package/dist/cjs/loaders/worker-agent.js +0 -24
  97. package/dist/esm/cdn/worker.js +0 -14
  98. package/dist/esm/loaders/worker-agent.js +0 -18
  99. package/dist/types/cdn/worker.d.ts +0 -2
  100. package/dist/types/cdn/worker.d.ts.map +0 -1
  101. package/dist/types/loaders/worker-agent.d.ts +0 -8
  102. package/dist/types/loaders/worker-agent.d.ts.map +0 -1
  103. package/src/cdn/worker.js +0 -21
  104. package/src/loaders/worker-agent.js +0 -24
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/ajax/aggregate/index.js"],"names":[],"mappings":"AAgBA;IACE,2BAAiC;IACjC,mDAgOC;IAzMC,qGAAwB;IACxB;;;;4BAAoC;IACpC;;;mBAA2E;CAwM9E;8BArO6B,4BAA4B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/ajax/aggregate/index.js"],"names":[],"mappings":"AAiBA;IACE,2BAAiC;IACjC,mDAyOC;IAlNC,qGAAwB;IACxB;;;;4BAAoC;IACpC;;;mBAA2E;CAiN9E;8BA/O6B,4BAA4B"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/page_view_event/aggregate/index.js"],"names":[],"mappings":"AAcA;IACE,2BAA2C;IAC3C,mDAoBC;IAjBC,wBAAwB;IACxB,8BAA8B;IAC9B,8BAA8B;IAiBhC,gBA8EC;CACF;8BA5G6B,4BAA4B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/page_view_event/aggregate/index.js"],"names":[],"mappings":"AAcA;IACE,2BAA2C;IAC3C,mDAoBC;IAjBC,wBAAwB;IACxB,8BAA8B;IAC9B,8BAA8B;IAiBhC,gBAgFC;CACF;8BA9G6B,4BAA4B"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/page_view_timing/aggregate/index.js"],"names":[],"mappings":"AAuBA;IACE,2BAAiC;IAMjC,mDAoCC;IAjCC,eAAiB;IACjB,mBAAqB;IACrB,4BAA+B;IAuB7B,4BAGQ;IAOZ;;;OAGG;IACH,6BAFW,MAAM,QAOhB;IAED;;OAEG;IACH,uCAUC;IAED,mDAqBC;IAED,qCAKC;IAED,gDAWC;IAGD;;;;kBAWC;IAGD,8BAuBC;;CACF;8BAnK6B,4BAA4B;iCANzB,2CAA2C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/page_view_timing/aggregate/index.js"],"names":[],"mappings":"AAuBA;IACE,2BAAiC;IAMjC,mDAoCC;IAjCC,eAAiB;IACjB,mBAAqB;IACrB,4BAA+B;IAuB7B,4BAGQ;IAOZ;;;OAGG;IACH,6BAFW,MAAM,QAOhB;IAED;;OAEG;IACH,uCAUC;IAED,mDAsBC;IAED,qCAKC;IAED,gDAWC;IAGD;;;;kBAWC;IAGD,8BAuBC;;CACF;8BApK6B,4BAA4B;iCANzB,2CAA2C"}
@@ -37,6 +37,7 @@ export class Aggregate extends AggregateBase {
37
37
  body?: undefined;
38
38
  } | {
39
39
  qs: {
40
+ fsh?: number | undefined;
40
41
  st: any;
41
42
  /** hr === "hasReplay" in NR1, standalone is always checked and processed before harvesting
42
43
  * so a race condition between ST and SR states should not be a concern if implemented here */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_trace/aggregate/index.js"],"names":[],"mappings":"AAiCA;IACE,2BAAiC;IAGjC,iEAyHC;IAvHC,kBAA+C;IAK/C,sBAAiD;IACjD,yBAAc;IACd,sBAAe;IACf,8BAAkB;IAClB,iCAAqB;IACrB,wBAA0G;IAC1G,wBAA4G;IAC5G;;4EAEwE;IACxE,kCAAyB;IAGzB,0CAAsC;IAuGxC,sEAcC;IA6CD,oDAOC;IAGD,oCAwBC;IAGD,uEAkBC;IAED,oDAKC;IAED,wBAwBC;IAED,uCAuBC;IAGD,gDASC;IAID,qCAkBC;IAGD,qEAUC;IAGD,mEAUC;IAGD,yBAeC;IAED;;;;OAIG;IACH,2BAHW,MAAM,GACJ,MAAM,CAsBlB;IAGD;;;;;;YA2BM;0GAC8F;;YAE9F;0FAC8E;;YAE9E,4IAA4I;;;;;;MAKjJ;IAED,mEA6BC;;CACF;8BAjf6B,4BAA4B;6BAH7B,2BAA2B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_trace/aggregate/index.js"],"names":[],"mappings":"AAiCA;IACE,2BAAiC;IAGjC,iEAyHC;IAvHC,kBAA+C;IAK/C,sBAAiD;IACjD,yBAAc;IACd,sBAAe;IACf,8BAAkB;IAClB,iCAAqB;IACrB,wBAA0G;IAC1G,wBAA4G;IAC5G;;4EAEwE;IACxE,kCAAyB;IAGzB,0CAAsC;IAuGxC,sEAcC;IA6CD,oDAOC;IAGD,oCAwBC;IAGD,uEAkBC;IAED,oDAKC;IAED,wBAwBC;IAED,uCAuBC;IAGD,gDASC;IAID,qCAkBC;IAGD,qEAUC;IAGD,mEAUC;IAGD,yBAeC;IAED;;;;OAIG;IACH,2BAHW,MAAM,GACJ,MAAM,CAsBlB;IAGD;;;;;;;YAkCM;0GAC8F;;YAE9F;0FAC8E;;YAE9E,4IAA4I;;;;;;MAMjJ;IAED,mEA6BC;;CACF;8BAzf6B,4BAA4B;6BAH7B,2BAA2B"}
@@ -17,6 +17,7 @@ export class Aggregate extends AggregateBase {
17
17
  harvestTimeSeconds: any;
18
18
  interactionsToHarvest: never[];
19
19
  interactionsSent: never[];
20
+ disableSpaFix: boolean;
20
21
  };
21
22
  serializer: Serializer;
22
23
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/spa/aggregate/index.js"],"names":[],"mappings":"AA4BA;IACE,2BAAiC;IACjC,mDA8rBC;IA3rBC;;;;;;;;;;;;;;;;MAgBC;IAED,uBAAsC;CA0qBzC;8BA1sB6B,4BAA4B;2BAJ/B,cAAc"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/spa/aggregate/index.js"],"names":[],"mappings":"AA4BA;IACE,2BAAiC;IACjC,mDAgsBC;IA7rBC;;;;;;;;;;;;;;;;;MAkBC;IAED,uBAAsC;CA0qBzC;8BA5sB6B,4BAA4B;2BAJ/B,cAAc"}
@@ -1 +1 @@
1
- {"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../../../../../src/features/spa/aggregate/serializer.js"],"names":[],"mappings":"AAUA;IAII;;;;;;OAMG;IACH,gBAFU,MAAM,GAAC,SAAS,CAEK;IAGjC,0EASC;IAED,oFAMC;IAED,iHAyJC;CACF;8BA9L6B,wCAAwC"}
1
+ {"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../../../../../src/features/spa/aggregate/serializer.js"],"names":[],"mappings":"AAUA;IAII;;;;;;OAMG;IACH,gBAFU,MAAM,GAAC,SAAS,CAEK;IAGjC,0EASC;IAED,oFAMC;IAED,iHA+JC;CACF;8BApM6B,wCAAwC"}
@@ -1,6 +1,5 @@
1
1
  export { Agent } from "./loaders/agent";
2
2
  export { BrowserAgent } from "./loaders/browser-agent";
3
- export { WorkerAgent } from "./loaders/worker-agent";
4
3
  export { MicroAgent } from "./loaders/micro-agent";
5
4
  export { Ajax } from "./features/ajax";
6
5
  export { JSErrors } from "./features/jserrors";
@@ -1 +1 @@
1
- {"version":3,"file":"public-path.npm.d.ts","sourceRoot":"","sources":["../../../../src/loaders/configure/public-path.npm.js"],"names":[],"mappings":"AACO,2CAEN"}
1
+ {"version":3,"file":"public-path.npm.d.ts","sourceRoot":"","sources":["../../../../src/loaders/configure/public-path.npm.js"],"names":[],"mappings":"AAAO,2CAEN"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newrelic/browser-agent",
3
- "version": "1.244.0",
3
+ "version": "1.246.0",
4
4
  "private": false,
5
5
  "author": "New Relic Browser Agent Team <browser-agent@newrelic.com>",
6
6
  "description": "New Relic Browser Agent",
@@ -26,9 +26,6 @@
26
26
  "loaders/micro-agent": [
27
27
  "dist/types/loaders/micro-agent.d.ts"
28
28
  ],
29
- "loaders/worker-agent": [
30
- "dist/types/loaders/worker-agent.d.ts"
31
- ],
32
29
  "features/ajax": [
33
30
  "dist/types/features/ajax/index.d.ts"
34
31
  ],
@@ -81,11 +78,6 @@
81
78
  "require": "./dist/cjs/loaders/micro-agent.js",
82
79
  "default": "./dist/esm/loaders/micro-agent.js"
83
80
  },
84
- "./loaders/worker-agent": {
85
- "types": "./dist/types/loaders/worker-agent.d.ts",
86
- "require": "./dist/cjs/loaders/worker-agent.js",
87
- "default": "./dist/esm/loaders/worker-agent.js"
88
- },
89
81
  "./features/ajax": {
90
82
  "types": "./dist/types/features/ajax/index.d.ts",
91
83
  "require": "./dist/cjs/features/ajax/index.js",
@@ -13,8 +13,12 @@ import 'core-js/stable/array/some'
13
13
  import 'core-js/stable/object/assign'
14
14
  import 'core-js/stable/object/entries'
15
15
  import 'core-js/stable/object/values'
16
+ import 'core-js/stable/object/from-entries'
16
17
  import 'core-js/stable/map'
17
18
  import 'core-js/stable/reflect'
18
19
  import 'core-js/stable/set'
19
20
  import 'core-js/stable/weak-set'
20
21
  import 'core-js/stable/object/get-own-property-descriptors'
22
+ import 'core-js/stable/url'
23
+ import 'core-js/stable/url-search-params'
24
+ import 'core-js/stable/string/starts-with'
@@ -13,7 +13,8 @@ export function getModeledObject (obj, model) {
13
13
  for (let key in target) {
14
14
  if (obj[key] !== undefined) {
15
15
  try {
16
- if (typeof obj[key] === 'object' && typeof model[key] === 'object') output[key] = getModeledObject(obj[key], model[key])
16
+ if (Array.isArray(obj[key]) && Array.isArray(model[key])) output[key] = Array.from(new Set([...obj[key], ...model[key]]))
17
+ else if (typeof obj[key] === 'object' && typeof model[key] === 'object') output[key] = getModeledObject(obj[key], model[key])
17
18
  else output[key] = obj[key]
18
19
  } catch (e) {
19
20
  warn('An error occurred while setting a property of a Configurable', e)
@@ -29,6 +29,7 @@ const model = () => {
29
29
  }
30
30
  }
31
31
  return {
32
+ feature_flags: [],
32
33
  proxy: {
33
34
  assets: undefined, // if this value is set, it will be used to overwrite the webpack asset path used to fetch assets
34
35
  beacon: undefined // likewise for the url to which we send analytics
@@ -25,9 +25,10 @@ const model = {
25
25
  inactiveAt: 0,
26
26
  expiresAt: 0,
27
27
  updatedAt: Date.now(),
28
- sessionReplay: MODE.OFF,
28
+ sessionReplayMode: MODE.OFF,
29
29
  sessionReplaySentFirstChunk: false,
30
30
  sessionTraceMode: MODE.OFF,
31
+ traceHarvestStarted: false,
31
32
  custom: {}
32
33
  }
33
34
  export const SESSION_EVENTS = {
@@ -140,7 +141,7 @@ export class SessionEntity {
140
141
 
141
142
  // The fact that the session is "new" or pre-existing is used in some places in the agent. Session Replay and Trace
142
143
  // can use this info to inform whether to trust a new sampling decision vs continue a previous tracking effort.
143
- if (this.isNew === undefined) this.isNew = !Object.keys(initialRead).length
144
+ this.isNew = !Object.keys(initialRead).length
144
145
  // if its a "new" session, we write to storage API with the default values. These values may change over the lifespan of the agent run.
145
146
  // we can use a modeled object here to help us know and manage what values are being used. -- see "model" above
146
147
  if (this.isNew) this.write(getModeledObject(this.state, model), true)
@@ -3,15 +3,9 @@
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
5
 
6
- import { globalScope, isBrowserScope } from '../constants/runtime'
7
-
8
- var stringsToParsedUrls = {}
6
+ import { globalScope } from '../constants/runtime'
9
7
 
10
8
  export function parseUrl (url) {
11
- if (url in stringsToParsedUrls) {
12
- return stringsToParsedUrls[url]
13
- }
14
-
15
9
  // Return if URL is a data URL, parseUrl assumes urls are http/https
16
10
  if ((url || '').indexOf('data:') === 0) {
17
11
  return {
@@ -19,52 +13,30 @@ export function parseUrl (url) {
19
13
  }
20
14
  }
21
15
 
22
- let urlEl
23
- var location = globalScope?.location
24
- var ret = {}
25
-
26
- if (isBrowserScope) {
27
- // Use an anchor dom element to resolve the url natively.
28
- urlEl = document.createElement('a')
29
- urlEl.href = url
30
- } else {
31
- try {
32
- urlEl = new URL(url, location.href)
33
- } catch (err) {
34
- return ret
16
+ try {
17
+ const parsedUrl = new URL(url, location.href)
18
+ const returnVal = {
19
+ port: parsedUrl.port,
20
+ hostname: parsedUrl.hostname,
21
+ pathname: parsedUrl.pathname,
22
+ search: parsedUrl.search,
23
+ protocol: parsedUrl.protocol.slice(0, parsedUrl.protocol.indexOf(':')),
24
+ sameOrigin: parsedUrl.protocol === globalScope?.location?.protocol && parsedUrl.host === globalScope?.location?.host
35
25
  }
36
- }
37
-
38
- ret.port = urlEl.port
39
-
40
- var firstSplit = urlEl.href.split('://')
41
-
42
- if (!ret.port && firstSplit[1]) {
43
- ret.port = firstSplit[1].split('/')[0].split('@').pop().split(':')[1]
44
- }
45
- if (!ret.port || ret.port === '0') ret.port = (firstSplit[0] === 'https' ? '443' : '80')
46
-
47
- // Host not provided in IE for relative urls
48
- ret.hostname = (urlEl.hostname || location.hostname)
49
-
50
- ret.pathname = urlEl.pathname
51
-
52
- ret.protocol = firstSplit[0]
53
26
 
54
- // Pathname sometimes doesn't have leading slash (IE 8 and 9)
55
- if (ret.pathname.charAt(0) !== '/') ret.pathname = '/' + ret.pathname
56
-
57
- // urlEl.protocol is ':' in old ie when protocol is not specified
58
- var sameProtocol = !urlEl.protocol || urlEl.protocol === ':' || urlEl.protocol === location.protocol
59
- var sameDomain = urlEl.hostname === location.hostname && urlEl.port === location.port
27
+ if (!returnVal.port || returnVal.port === '') {
28
+ if (parsedUrl.protocol === 'http:') returnVal.port = '80'
29
+ if (parsedUrl.protocol === 'https:') returnVal.port = '443'
30
+ }
60
31
 
61
- // urlEl.hostname is not provided by IE for relative urls, but relative urls are also same-origin
62
- ret.sameOrigin = sameProtocol && (!urlEl.hostname || sameDomain)
32
+ if (!returnVal.pathname || returnVal.pathname === '') {
33
+ returnVal.pathname = '/'
34
+ } else if (!returnVal.pathname.startsWith('/')) {
35
+ returnVal.pathname = `/${returnVal.pathname}`
36
+ }
63
37
 
64
- // Only cache if url doesn't have a path
65
- if (ret.pathname === '/') {
66
- stringsToParsedUrls[url] = ret
38
+ return returnVal
39
+ } catch (err) {
40
+ return {}
67
41
  }
68
-
69
- return ret
70
42
  }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Tests a passed object to see if it is a pure object or not. All non-primatives in JS
3
+ * are technically objects and would pass a `typeof` check.
4
+ * @param {*} obj Input object to be tested
5
+ **/
6
+ export function isPureObject (obj) {
7
+ return obj?.constructor === ({}).constructor
8
+ }
@@ -13,8 +13,7 @@ if (isBrowserScope) {
13
13
  firstInputDelay.update({
14
14
  value: entries[0].startTime,
15
15
  entries,
16
- attrs: { type: entries[0].name, fid: Math.round(value) },
17
- shouldAddConnectionAttributes: true
16
+ attrs: { type: entries[0].name, fid: Math.round(value) }
18
17
  })
19
18
  })
20
19
  }
@@ -22,8 +22,7 @@ if (isBrowserScope) {
22
22
  ...(!!lcpEntry.url && { elUrl: cleanURL(lcpEntry.url) }),
23
23
  ...(!!lcpEntry.element?.tagName && { elTag: lcpEntry.element.tagName })
24
24
  }
25
- }),
26
- shouldAddConnectionAttributes: true
25
+ })
27
26
  })
28
27
  })
29
28
  }
@@ -8,7 +8,7 @@ export class VitalMetric {
8
8
  this.roundingMethod = typeof roundingMethod === 'function' ? roundingMethod : Math.floor
9
9
  }
10
10
 
11
- update ({ value, entries = [], attrs = {}, shouldAddConnectionAttributes = false }) {
11
+ update ({ value, entries = [], attrs = {} }) {
12
12
  if (value < 0) return
13
13
  const state = {
14
14
  value: this.roundingMethod(value),
@@ -16,7 +16,7 @@ export class VitalMetric {
16
16
  entries,
17
17
  attrs
18
18
  }
19
- if (shouldAddConnectionAttributes) addConnectionAttributes(state.attrs)
19
+
20
20
  this.history.push(state)
21
21
  this.#subscribers.forEach(cb => {
22
22
  try {
@@ -48,13 +48,3 @@ export class VitalMetric {
48
48
  return () => { this.#subscribers.delete(callback) }
49
49
  }
50
50
  }
51
-
52
- function addConnectionAttributes (obj) {
53
- var connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection // to date, both window & worker shares the same support for connection
54
- if (!connection) return
55
-
56
- if (connection.type) obj['net-type'] = connection.type
57
- if (connection.effectiveType) obj['net-etype'] = connection.effectiveType
58
- if (connection.rtt) obj['net-rtt'] = connection.rtt
59
- if (connection.downlink) obj['net-dlink'] = connection.downlink
60
- }
@@ -0,0 +1,95 @@
1
+ import { isPureObject } from '../../../common/util/type-check'
2
+
3
+ /**
4
+ * @typedef {object} GQLMetadata
5
+ * @property {string} operationName Name of the operation
6
+ * @property {string} operationType Type of the operation
7
+ * @property {string} operationFramework Framework responsible for the operation
8
+ */
9
+
10
+ /**
11
+ * Parses and returns the graphql metadata from a network request. If the network
12
+ * request is not a graphql call, undefined will be returned.
13
+ * @param {object|string} body Ajax request body
14
+ * @param {string} query Ajax request query param string
15
+ * @returns {GQLMetadata | undefined}
16
+ */
17
+ export function parseGQL ({ body, query } = {}) {
18
+ if (!body && !query) return
19
+ try {
20
+ const gqlBody = parseBatchGQL(parseGQLContents(body))
21
+ if (gqlBody) return gqlBody
22
+ const gqlQuery = parseSingleGQL(parseGQLQueryString(query))
23
+ if (gqlQuery) return gqlQuery
24
+ } catch (err) {
25
+ // parsing failed, return undefined
26
+ }
27
+ }
28
+
29
+ /**
30
+ * @param {string|Object} gql The GraphQL object body sent to a GQL server
31
+ * @returns {GQLMetadata}
32
+ */
33
+ function parseSingleGQL (contents) {
34
+ if (typeof contents !== 'object' || !contents.query || typeof contents.query !== 'string') return
35
+
36
+ /** parses gql query string and returns [fullmatch, type match, name match] */
37
+ const matches = contents.query.trim().match(/^(query|mutation|subscription)\s?(\w*)/)
38
+ const operationType = matches?.[1]
39
+ if (!operationType) return
40
+ const operationName = contents.operationName || matches?.[2] || 'Anonymous'
41
+ return {
42
+ operationName, // the operation name of the indiv query
43
+ operationType, // query, mutation, or subscription,
44
+ operationFramework: 'GraphQL'
45
+ }
46
+ }
47
+
48
+ function parseBatchGQL (contents) {
49
+ if (!contents) return
50
+ if (!Array.isArray(contents)) contents = [contents]
51
+
52
+ const opNames = []
53
+ const opTypes = []
54
+ for (let content of contents) {
55
+ const operation = parseSingleGQL(content)
56
+ if (!operation) continue
57
+
58
+ opNames.push(operation.operationName)
59
+ opTypes.push(operation.operationType)
60
+ }
61
+
62
+ if (!opTypes.length) return
63
+ return {
64
+ operationName: opNames.join(','), // the operation name of the indiv query -- joined by ',' for batched results
65
+ operationType: opTypes.join(','), // query, mutation, or subscription -- joined by ',' for batched results
66
+ operationFramework: 'GraphQL'
67
+ }
68
+ }
69
+
70
+ function parseGQLContents (gqlContents) {
71
+ let contents
72
+
73
+ if (!gqlContents || (typeof gqlContents !== 'string' && typeof gqlContents !== 'object')) return
74
+ else if (typeof gqlContents === 'string') contents = JSON.parse(gqlContents)
75
+ else contents = gqlContents
76
+
77
+ if (!isPureObject(contents) && !Array.isArray(contents)) return
78
+
79
+ let isValid = false
80
+ if (Array.isArray(contents)) isValid = contents.some(x => validateGQLObject(x))
81
+ else isValid = validateGQLObject(contents)
82
+
83
+ if (!isValid) return
84
+ return contents
85
+ }
86
+
87
+ function parseGQLQueryString (gqlQueryString) {
88
+ if (!gqlQueryString || typeof gqlQueryString !== 'string') return
89
+ const params = new URLSearchParams(gqlQueryString)
90
+ return parseGQLContents(Object.fromEntries(params))
91
+ }
92
+
93
+ function validateGQLObject (obj) {
94
+ return !(typeof obj !== 'object' || !obj.query || typeof obj.query !== 'string')
95
+ }
@@ -13,6 +13,7 @@ import { FEATURE_NAME } from '../constants'
13
13
  import { FEATURE_NAMES } from '../../../loaders/features/features'
14
14
  import { SUPPORTABILITY_METRIC_CHANNEL } from '../../metrics/constants'
15
15
  import { AggregateBase } from '../../utils/aggregate-base'
16
+ import { parseGQL } from './gql'
16
17
 
17
18
  export class Aggregate extends AggregateBase {
18
19
  static featureName = FEATURE_NAME
@@ -119,6 +120,14 @@ export class Aggregate extends AggregateBase {
119
120
  event.spanTimestamp = xhrContext.dt.timestamp
120
121
  }
121
122
 
123
+ // parsed from the AJAX body, looking for operationName param & parsing query for operationType
124
+ event.gql = params.gql = parseGQL({
125
+ body: this.body,
126
+ query: this?.parsedOrigin?.search
127
+ })
128
+
129
+ if (event.gql) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Ajax/Events/GraphQL/Bytes-Added', stringify(event.gql).length], undefined, FEATURE_NAMES.metrics, ee)
130
+
122
131
  // if the ajax happened inside an interaction, hold it until the interaction finishes
123
132
  if (this.spaNode) {
124
133
  var interactionId = this.spaNode.interaction.id
@@ -221,7 +230,8 @@ export class Aggregate extends AggregateBase {
221
230
  var insert = '2,'
222
231
 
223
232
  // add custom attributes
224
- var attrParts = addCustomAttributes(getInfo(agentIdentifier).jsAttributes || {}, this.addString)
233
+ // gql decorators are added as custom attributes to alleviate need for new BEL schema
234
+ var attrParts = addCustomAttributes({ ...(getInfo(agentIdentifier).jsAttributes || {}), ...(event.gql || {}) }, this.addString)
225
235
  fields.unshift(numeric(attrParts.length))
226
236
 
227
237
  insert += fields.join(',')
@@ -157,6 +157,8 @@ function subscribeToEvents (agentIdentifier, ee, handler, dt) {
157
157
 
158
158
  this.startTime = now()
159
159
 
160
+ this.body = data
161
+
160
162
  this.listener = function (evt) {
161
163
  try {
162
164
  if (evt.type === 'abort' && !(context.loadCaptureCalled)) {
@@ -329,6 +331,7 @@ function subscribeToEvents (agentIdentifier, ee, handler, dt) {
329
331
  var method = ('' + ((target && target instanceof origRequest && target.method) ||
330
332
  opts.method || 'GET')).toUpperCase()
331
333
  this.params.method = method
334
+ this.body = opts.body
332
335
 
333
336
  this.txSize = dataSize(opts.body) || 0
334
337
  }
@@ -191,7 +191,7 @@ export class Aggregate extends AggregateBase {
191
191
  this.pageviewReported[bucketHash] = true
192
192
  }
193
193
 
194
- if (agentRuntime?.session?.state?.sessionReplay) params.hasReplay = true
194
+ if (agentRuntime?.session?.state?.sessionReplayMode) params.hasReplay = true
195
195
  params.firstOccurrenceTimestamp = this.observedAt[bucketHash]
196
196
 
197
197
  var type = internal ? 'ierr' : 'err'
@@ -69,6 +69,8 @@ export class Aggregate extends AggregateBase {
69
69
  at: info.atts
70
70
  }
71
71
 
72
+ if (agentRuntime.session) queryParameters.fsh = Number(agentRuntime.session.isNew) // "first session harvest" aka RUM request or PageView event of a session
73
+
72
74
  let body
73
75
  if (typeof info.jsAttributes === 'object' && Object.keys(info.jsAttributes).length > 0) {
74
76
  body = { ja: info.jsAttributes }
@@ -94,6 +94,7 @@ export class Aggregate extends AggregateBase {
94
94
 
95
95
  addTiming (name, value, attrs) {
96
96
  attrs = attrs || {}
97
+ addConnectionAttributes(attrs) // network conditions may differ from the actual for VitalMetrics when they were captured
97
98
 
98
99
  // If cls was set to another value by `onCLS`, then it's supported and is attached onto any timing but is omitted until such time.
99
100
  /*
@@ -175,3 +176,13 @@ export class Aggregate extends AggregateBase {
175
176
  return payload
176
177
  }
177
178
  }
179
+
180
+ function addConnectionAttributes (obj) {
181
+ var connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection // to date, both window & worker shares the same support for connection
182
+ if (!connection) return
183
+
184
+ if (connection.type) obj['net-type'] = connection.type
185
+ if (connection.effectiveType) obj['net-etype'] = connection.effectiveType
186
+ if (connection.rtt) obj['net-rtt'] = connection.rtt
187
+ if (connection.downlink) obj['net-dlink'] = connection.downlink
188
+ }
@@ -133,14 +133,14 @@ export class Aggregate extends AggregateBase {
133
133
  this.ee.on(SESSION_EVENTS.RESUME, () => {
134
134
  // if the mode changed on a different tab, it needs to update this instance to match
135
135
  const { session } = getRuntime(this.agentIdentifier)
136
- this.mode = session.state.sessionReplay
136
+ this.mode = session.state.sessionReplayMode
137
137
  if (!this.initialized || this.mode === MODE.OFF) return
138
138
  this.startRecording()
139
139
  })
140
140
 
141
141
  this.ee.on(SESSION_EVENTS.UPDATE, (type, data) => {
142
142
  if (!this.initialized || this.blocked || type !== SESSION_EVENT_TYPES.CROSS_TAB) return
143
- if (this.mode !== MODE.OFF && data.sessionReplay === MODE.OFF) this.abort(ABORT_REASONS.CROSS_TAB)
143
+ if (this.mode !== MODE.OFF && data.sessionReplayMode === MODE.OFF) this.abort(ABORT_REASONS.CROSS_TAB)
144
144
  this.mode = data.sessionReplay
145
145
  })
146
146
 
@@ -167,7 +167,7 @@ export class Aggregate extends AggregateBase {
167
167
 
168
168
  this.scheduler.startTimer(this.harvestTimeSeconds)
169
169
 
170
- this.syncWithSessionManager({ sessionReplay: this.mode })
170
+ this.syncWithSessionManager({ sessionReplayMode: this.mode })
171
171
  }
172
172
  }
173
173
  }, this.featureName, this.ee)
@@ -201,7 +201,7 @@ export class Aggregate extends AggregateBase {
201
201
  // session replay samples can only be decided on the first load of a session
202
202
  // session replays can continue if already in progress
203
203
  if (!session.isNew) { // inherit the mode of the existing session
204
- this.mode = session.state.sessionReplay
204
+ this.mode = session.state.sessionReplayMode
205
205
  } else {
206
206
  // The session is new... determine the mode the new session should start in
207
207
  if (fullSample) this.mode = MODE.FULL // full mode has precedence over error mode
@@ -241,7 +241,7 @@ export class Aggregate extends AggregateBase {
241
241
  }
242
242
  this.startRecording()
243
243
 
244
- this.syncWithSessionManager({ sessionReplay: this.mode })
244
+ this.syncWithSessionManager({ sessionReplayMode: this.mode })
245
245
  }
246
246
 
247
247
  prepareHarvest () {
@@ -439,7 +439,7 @@ export class Aggregate extends AggregateBase {
439
439
  this.blocked = true
440
440
  this.mode = MODE.OFF
441
441
  this.stopRecording()
442
- this.syncWithSessionManager({ sessionReplay: this.mode })
442
+ this.syncWithSessionManager({ sessionReplayMode: this.mode })
443
443
  this.clearTimestamps()
444
444
  this.ee.emit('REPLAY_ABORTED')
445
445
  }
@@ -125,7 +125,7 @@ export class Aggregate extends AggregateBase {
125
125
  this.ee.on(SESSION_EVENTS.PAUSE, () => { mostRecentModeKnown = sessionEntity.state.sessionTraceMode })
126
126
 
127
127
  if (!sessionEntity.isNew) { // inherit the same mode as existing session's Trace
128
- if (sessionEntity.state.sessionReplay === MODE.OFF) this.isStandalone = true
128
+ if (sessionEntity.state.sessionReplayMode === MODE.OFF) this.isStandalone = true
129
129
  controlTraceOp(mostRecentModeKnown = sessionEntity.state.sessionTraceMode)
130
130
  } else { // for new sessions, see the truth table associated with NEWRELIC-8662 wrt the new Trace behavior under session management
131
131
  const replayMode = await getSessionReplayMode(agentIdentifier)
@@ -462,6 +462,13 @@ export class Aggregate extends AggregateBase {
462
462
  this.trace = {}
463
463
  this.nodeCount = 0
464
464
 
465
+ let firstHarvestOfSession
466
+ if (this.agentRuntime.session) {
467
+ const isFirstPayload = !this.agentRuntime.session.state.traceHarvestStarted
468
+ firstHarvestOfSession = { fsh: Number(isFirstPayload) } // converted to '0' | '1'
469
+ if (isFirstPayload) this.agentRuntime.session.write({ traceHarvestStarted: true })
470
+ }
471
+
465
472
  return {
466
473
  qs: {
467
474
  st: this.agentRuntime.offset,
@@ -472,7 +479,8 @@ export class Aggregate extends AggregateBase {
472
479
  * so that blob parsing doesn't need to happen to support UI/API functions */
473
480
  fts: this.agentRuntime.offset + earliestTimeStamp,
474
481
  /** n === "nodeCount" in NR1, a count of nodes in the ST payload, so that blob parsing doesn't need to happen to support UI/API functions */
475
- n: stns.length // node count
482
+ n: stns.length, // node count
483
+ ...firstHarvestOfSession
476
484
  },
477
485
  body: { res: stns }
478
486
  }
@@ -46,7 +46,9 @@ export class Aggregate extends AggregateBase {
46
46
  depth: 0,
47
47
  harvestTimeSeconds: getConfigurationValue(agentIdentifier, 'spa.harvestTimeSeconds') || 10,
48
48
  interactionsToHarvest: [],
49
- interactionsSent: []
49
+ interactionsSent: [],
50
+ // The below feature flag is used to disable the SPA ajax fix for specific customers, see https://new-relic.atlassian.net/browse/NR-172169
51
+ disableSpaFix: (getConfigurationValue(agentIdentifier, 'feature_flags') || []).indexOf('disable-spa-fix') > -1
50
52
  }
51
53
 
52
54
  this.serializer = new Serializer(this)
@@ -279,7 +281,7 @@ export class Aggregate extends AggregateBase {
279
281
  // context is stored on the xhr and is shared with all callbacks associated
280
282
  // with the new xhr
281
283
  register('new-xhr', function () {
282
- if (!state.currentNode && state.prevInteraction && !state.prevInteraction.ignored) {
284
+ if (!state.disableSpaFix && !state.currentNode && state.prevInteraction && !state.prevInteraction.ignored) {
283
285
  /*
284
286
  * The previous interaction was discarded before a route change. Restore the interaction
285
287
  * in case this XHR is associated with a route change.
@@ -382,7 +384,7 @@ export class Aggregate extends AggregateBase {
382
384
 
383
385
  register(FETCH_START, function (fetchArguments, dtPayload) {
384
386
  if (fetchArguments) {
385
- if (!state.currentNode && state.prevInteraction && !state.prevInteraction.ignored) {
387
+ if (!state.disableSpaFix && !state.currentNode && state.prevInteraction && !state.prevInteraction.ignored) {
386
388
  /*
387
389
  * The previous interaction was discarded before a route change. Restore the interaction
388
390
  * in case this XHR is associated with a route change.
@@ -102,7 +102,6 @@ export class Serializer extends SharedContext {
102
102
  nullable(attrs.firstPaint, numeric, true) +
103
103
  nullable(attrs.firstContentfulPaint, numeric, false)
104
104
  )
105
-
106
105
  var attrParts = addCustomAttributes(attrs.custom, addString)
107
106
  children = children.concat(attrParts)
108
107
  attrCount = attrParts.length
@@ -128,6 +127,13 @@ export class Serializer extends SharedContext {
128
127
  nullable(node.dt && node.dt.traceId, addString, true) +
129
128
  nullable(node.dt && node.dt.timestamp, numeric, false)
130
129
  )
130
+
131
+ // add params.gql here
132
+ if (Object.keys(params?.gql || {}).length) {
133
+ var ajaxAttrParts = addCustomAttributes(params.gql, addString)
134
+ children = children.concat(ajaxAttrParts)
135
+ attrCount = ajaxAttrParts.length
136
+ }
131
137
  break
132
138
 
133
139
  case 4:
@@ -129,7 +129,7 @@ export class InstrumentBase extends FeatureBase {
129
129
  if (featureName === FEATURE_NAMES.sessionReplay) {
130
130
  if (!originals.MO) return false // Session Replay cannot work without Mutation Observer
131
131
  if (getConfigurationValue(this.agentIdentifier, 'session_trace.enabled') === false) return false // Session Replay as of now is tightly coupled with Session Trace in the UI
132
- return !!session?.isNew || !!session?.state.sessionReplay // Session Replay should only try to run if already running from a previous page, or at the beginning of a session
132
+ return !!session?.isNew || !!session?.state.sessionReplayMode // Session Replay should only try to run if already running from a previous page, or at the beginning of a session
133
133
  }
134
134
  return true
135
135
  }