@newrelic/browser-agent 1.234.0 → 1.235.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 (196) hide show
  1. package/README.md +1 -1
  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/constants/shared-channel.js +19 -0
  5. package/dist/cjs/common/harvest/harvest-scheduler.js +21 -5
  6. package/dist/cjs/common/session/{session-entity.test.js → session-entity.component-test.js} +79 -42
  7. package/dist/cjs/common/session/session-entity.js +19 -11
  8. package/dist/cjs/common/timer/interaction-timer.js +1 -1
  9. package/dist/cjs/common/url/canonicalize-url.test.js +26 -30
  10. package/dist/cjs/common/util/data-size.test.js +37 -20
  11. package/dist/cjs/common/util/feature-flags.js +23 -12
  12. package/dist/cjs/common/util/feature-flags.test.js +84 -0
  13. package/dist/cjs/common/util/global-scope.js +1 -32
  14. package/dist/cjs/common/util/global-scope.test.js +72 -0
  15. package/dist/cjs/common/util/obfuscate.component-test.js +129 -0
  16. package/dist/cjs/common/util/obfuscate.js +2 -2
  17. package/dist/cjs/common/util/submit-data.js +3 -3
  18. package/dist/cjs/common/util/submit-data.test.js +145 -121
  19. package/dist/cjs/common/wrap/wrap-raf.js +1 -1
  20. package/dist/cjs/common/wrap/wrap-timer.js +1 -1
  21. package/dist/cjs/features/jserrors/aggregate/index.js +4 -0
  22. package/dist/cjs/features/jserrors/instrument/index.js +2 -15
  23. package/dist/cjs/features/session_replay/aggregate/index.component-test.js +457 -0
  24. package/dist/cjs/features/session_replay/aggregate/index.js +99 -82
  25. package/dist/cjs/features/session_replay/replay-mode.js +28 -0
  26. package/dist/cjs/features/session_trace/aggregate/index.js +222 -99
  27. package/dist/cjs/features/session_trace/constants.js +1 -3
  28. package/dist/cjs/features/session_trace/instrument/index.js +0 -16
  29. package/dist/cjs/features/spa/constants.js +0 -1
  30. package/dist/cjs/features/utils/agent-session.js +20 -36
  31. package/dist/cjs/features/utils/agent-session.test.js +211 -0
  32. package/dist/cjs/features/utils/aggregate-base.js +7 -12
  33. package/dist/cjs/features/utils/aggregate-base.test.js +110 -0
  34. package/dist/cjs/features/utils/feature-base.test.js +42 -0
  35. package/dist/cjs/features/utils/handler-cache.js +28 -23
  36. package/dist/cjs/features/utils/handler-cache.test.js +53 -0
  37. package/dist/cjs/features/utils/instrument-base.js +58 -39
  38. package/dist/cjs/features/utils/instrument-base.test.js +179 -0
  39. package/dist/cjs/features/utils/lazy-feature-loader.test.js +30 -0
  40. package/dist/cjs/loaders/agent.js +0 -1
  41. package/dist/cjs/loaders/api/api.js +1 -1
  42. package/dist/cjs/loaders/features/featureDependencies.js +2 -0
  43. package/dist/esm/common/constants/env.cdn.js +1 -1
  44. package/dist/esm/common/constants/env.npm.js +1 -1
  45. package/dist/esm/common/constants/shared-channel.js +12 -0
  46. package/dist/esm/common/harvest/harvest-scheduler.js +21 -5
  47. package/dist/esm/common/session/{session-entity.test.js → session-entity.component-test.js} +77 -40
  48. package/dist/esm/common/session/session-entity.js +17 -11
  49. package/dist/esm/common/timer/interaction-timer.js +1 -1
  50. package/dist/esm/common/url/canonicalize-url.test.js +25 -29
  51. package/dist/esm/common/util/data-size.test.js +35 -20
  52. package/dist/esm/common/util/feature-flags.js +23 -12
  53. package/dist/esm/common/util/feature-flags.test.js +80 -0
  54. package/dist/esm/common/util/global-scope.js +1 -29
  55. package/dist/esm/common/util/global-scope.test.js +70 -0
  56. package/dist/esm/common/util/obfuscate.component-test.js +125 -0
  57. package/dist/esm/common/util/obfuscate.js +2 -2
  58. package/dist/esm/common/util/submit-data.js +3 -3
  59. package/dist/esm/common/util/submit-data.test.js +143 -121
  60. package/dist/esm/common/wrap/wrap-raf.js +1 -1
  61. package/dist/esm/common/wrap/wrap-timer.js +1 -1
  62. package/dist/esm/features/jserrors/aggregate/index.js +4 -0
  63. package/dist/esm/features/jserrors/instrument/index.js +2 -15
  64. package/dist/esm/features/session_replay/aggregate/index.component-test.js +453 -0
  65. package/dist/esm/features/session_replay/aggregate/index.js +92 -78
  66. package/dist/esm/features/session_replay/replay-mode.js +23 -0
  67. package/dist/esm/features/session_trace/aggregate/index.js +223 -100
  68. package/dist/esm/features/session_trace/constants.js +0 -1
  69. package/dist/esm/features/session_trace/instrument/index.js +1 -17
  70. package/dist/esm/features/spa/constants.js +0 -1
  71. package/dist/esm/features/utils/agent-session.js +21 -37
  72. package/dist/esm/features/utils/agent-session.test.js +207 -0
  73. package/dist/esm/features/utils/aggregate-base.js +7 -12
  74. package/dist/esm/features/utils/aggregate-base.test.js +108 -0
  75. package/dist/esm/features/utils/feature-base.test.js +40 -0
  76. package/dist/esm/features/utils/handler-cache.js +28 -23
  77. package/dist/esm/features/utils/handler-cache.test.js +51 -0
  78. package/dist/esm/features/utils/instrument-base.js +58 -39
  79. package/dist/esm/features/utils/instrument-base.test.js +175 -0
  80. package/dist/esm/features/utils/lazy-feature-loader.test.js +29 -0
  81. package/dist/esm/loaders/agent.js +0 -1
  82. package/dist/esm/loaders/api/api.js +2 -2
  83. package/dist/esm/loaders/features/featureDependencies.js +2 -0
  84. package/dist/types/common/constants/shared-channel.d.ts +5 -0
  85. package/dist/types/common/constants/shared-channel.d.ts.map +1 -0
  86. package/dist/types/common/harvest/harvest-scheduler.component-test.d.ts +2 -0
  87. package/dist/types/common/harvest/harvest-scheduler.component-test.d.ts.map +1 -0
  88. package/dist/types/common/harvest/harvest-scheduler.d.ts +4 -0
  89. package/dist/types/common/harvest/harvest-scheduler.d.ts.map +1 -1
  90. package/dist/types/common/harvest/harvest.component-test.d.ts +2 -0
  91. package/dist/types/common/harvest/harvest.component-test.d.ts.map +1 -0
  92. package/dist/types/common/session/session-entity.component-test.d.ts +2 -0
  93. package/dist/types/common/session/session-entity.component-test.d.ts.map +1 -0
  94. package/dist/types/common/session/session-entity.d.ts +9 -5
  95. package/dist/types/common/session/session-entity.d.ts.map +1 -1
  96. package/dist/types/common/timer/interaction-timer.component-test.d.ts +2 -0
  97. package/dist/types/common/timer/interaction-timer.component-test.d.ts.map +1 -0
  98. package/dist/types/common/url/encode.component-test.d.ts +2 -0
  99. package/dist/types/common/url/encode.component-test.d.ts.map +1 -0
  100. package/dist/types/common/url/protocol.component-test.d.ts +2 -0
  101. package/dist/types/common/url/protocol.component-test.d.ts.map +1 -0
  102. package/dist/types/common/util/feature-flags.d.ts +1 -0
  103. package/dist/types/common/util/feature-flags.d.ts.map +1 -1
  104. package/dist/types/common/util/global-scope.d.ts +0 -9
  105. package/dist/types/common/util/global-scope.d.ts.map +1 -1
  106. package/dist/types/common/util/obfuscate.component-test.d.ts +2 -0
  107. package/dist/types/common/util/obfuscate.component-test.d.ts.map +1 -0
  108. package/dist/types/features/jserrors/aggregate/index.d.ts +1 -0
  109. package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
  110. package/dist/types/features/session_replay/aggregate/index.component-test.d.ts +2 -0
  111. package/dist/types/features/session_replay/aggregate/index.component-test.d.ts.map +1 -0
  112. package/dist/types/features/session_replay/aggregate/index.d.ts +14 -5
  113. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  114. package/dist/types/features/session_replay/instrument/index.d.ts.map +1 -1
  115. package/dist/types/features/session_replay/replay-mode.d.ts +9 -0
  116. package/dist/types/features/session_replay/replay-mode.d.ts.map +1 -0
  117. package/dist/types/features/session_trace/aggregate/index.d.ts +21 -3
  118. package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
  119. package/dist/types/features/session_trace/constants.d.ts +0 -1
  120. package/dist/types/features/session_trace/constants.d.ts.map +1 -1
  121. package/dist/types/features/session_trace/instrument/index.d.ts +0 -2
  122. package/dist/types/features/session_trace/instrument/index.d.ts.map +1 -1
  123. package/dist/types/features/spa/constants.d.ts.map +1 -1
  124. package/dist/types/features/utils/agent-session.d.ts.map +1 -1
  125. package/dist/types/features/utils/aggregate-base.d.ts +6 -1
  126. package/dist/types/features/utils/aggregate-base.d.ts.map +1 -1
  127. package/dist/types/features/utils/handler-cache.d.ts +12 -11
  128. package/dist/types/features/utils/handler-cache.d.ts.map +1 -1
  129. package/dist/types/features/utils/instrument-base.d.ts +17 -1
  130. package/dist/types/features/utils/instrument-base.d.ts.map +1 -1
  131. package/dist/types/loaders/agent.d.ts.map +1 -1
  132. package/dist/types/loaders/features/featureDependencies.d.ts.map +1 -1
  133. package/package.json +9 -7
  134. package/src/common/constants/shared-channel.js +13 -0
  135. package/src/common/harvest/harvest-scheduler.js +17 -6
  136. package/src/common/session/{session-entity.test.js → session-entity.component-test.js} +70 -47
  137. package/src/common/session/session-entity.js +15 -12
  138. package/src/common/timer/interaction-timer.js +1 -1
  139. package/src/common/url/canonicalize-url.test.js +32 -21
  140. package/src/common/util/data-size.test.js +27 -20
  141. package/src/common/util/feature-flags.js +24 -12
  142. package/src/common/util/feature-flags.test.js +98 -0
  143. package/src/common/util/global-scope.js +0 -26
  144. package/src/common/util/global-scope.test.js +87 -0
  145. package/src/common/util/obfuscate.component-test.js +173 -0
  146. package/src/common/util/obfuscate.js +2 -2
  147. package/src/common/util/submit-data.js +3 -3
  148. package/src/common/util/submit-data.test.js +123 -115
  149. package/src/common/wrap/wrap-raf.js +1 -1
  150. package/src/common/wrap/wrap-timer.js +1 -1
  151. package/src/features/jserrors/aggregate/index.js +5 -0
  152. package/src/features/jserrors/instrument/index.js +2 -15
  153. package/src/features/session_replay/aggregate/index.component-test.js +368 -0
  154. package/src/features/session_replay/aggregate/index.js +96 -71
  155. package/src/features/session_replay/instrument/index.js +0 -1
  156. package/src/features/session_replay/replay-mode.js +23 -0
  157. package/src/features/session_trace/aggregate/index.js +198 -79
  158. package/src/features/session_trace/constants.js +0 -1
  159. package/src/features/session_trace/instrument/index.js +2 -19
  160. package/src/features/spa/constants.js +0 -1
  161. package/src/features/utils/agent-session.js +22 -34
  162. package/src/features/utils/agent-session.test.js +194 -0
  163. package/src/features/utils/aggregate-base.js +12 -9
  164. package/src/features/utils/aggregate-base.test.js +122 -0
  165. package/src/features/utils/feature-base.test.js +45 -0
  166. package/src/features/utils/handler-cache.js +29 -23
  167. package/src/features/utils/handler-cache.test.js +72 -0
  168. package/src/features/utils/instrument-base.js +45 -29
  169. package/src/features/utils/instrument-base.test.js +190 -0
  170. package/src/features/utils/lazy-feature-loader.test.js +37 -0
  171. package/src/loaders/agent.js +0 -1
  172. package/src/loaders/api/api.js +2 -2
  173. package/src/loaders/features/featureDependencies.js +2 -0
  174. package/dist/cjs/common/storage/local-memory.js +0 -35
  175. package/dist/cjs/common/storage/local-memory.test.js +0 -20
  176. package/dist/esm/common/storage/local-memory.js +0 -28
  177. package/dist/esm/common/storage/local-memory.test.js +0 -18
  178. package/dist/types/common/storage/local-memory.d.ts +0 -8
  179. package/dist/types/common/storage/local-memory.d.ts.map +0 -1
  180. package/src/common/storage/local-memory.js +0 -30
  181. package/src/common/storage/local-memory.test.js +0 -19
  182. /package/dist/cjs/common/harvest/{harvest-scheduler.test.js → harvest-scheduler.component-test.js} +0 -0
  183. /package/dist/cjs/common/harvest/{harvest.test.js → harvest.component-test.js} +0 -0
  184. /package/dist/cjs/common/timer/{interaction-timer.test.js → interaction-timer.component-test.js} +0 -0
  185. /package/dist/cjs/common/url/{encode.test.js → encode.component-test.js} +0 -0
  186. /package/dist/cjs/common/url/{protocol.test.js → protocol.component-test.js} +0 -0
  187. /package/dist/esm/common/harvest/{harvest-scheduler.test.js → harvest-scheduler.component-test.js} +0 -0
  188. /package/dist/esm/common/harvest/{harvest.test.js → harvest.component-test.js} +0 -0
  189. /package/dist/esm/common/timer/{interaction-timer.test.js → interaction-timer.component-test.js} +0 -0
  190. /package/dist/esm/common/url/{encode.test.js → encode.component-test.js} +0 -0
  191. /package/dist/esm/common/url/{protocol.test.js → protocol.component-test.js} +0 -0
  192. /package/src/common/harvest/{harvest-scheduler.test.js → harvest-scheduler.component-test.js} +0 -0
  193. /package/src/common/harvest/{harvest.test.js → harvest.component-test.js} +0 -0
  194. /package/src/common/timer/{interaction-timer.test.js → interaction-timer.component-test.js} +0 -0
  195. /package/src/common/url/{encode.test.js → encode.component-test.js} +0 -0
  196. /package/src/common/url/{protocol.test.js → protocol.component-test.js} +0 -0
@@ -1,34 +1,45 @@
1
+ import { faker } from '@faker-js/faker'
2
+ import * as globalScopeModule from '../util/global-scope'
3
+ import * as cleanUrlModule from './clean-url'
4
+ import { canonicalizeUrl } from './canonicalize-url'
5
+
6
+ jest.mock('../util/global-scope')
7
+ jest.mock('./clean-url')
8
+
9
+ beforeEach(() => {
10
+ jest.spyOn(cleanUrlModule, 'cleanURL').mockImplementation(input => input)
11
+ jest.replaceProperty(globalScopeModule, 'initialLocation', faker.internet.url())
12
+ })
13
+
1
14
  afterEach(() => {
2
- jest.resetModules()
15
+ jest.resetAllMocks()
3
16
  })
4
17
 
5
18
  test.each([null, undefined, 34])('returns empty string when url argument is %s', async (url) => {
6
- const { canonicalizeUrl } = await import('./canonicalize-url')
7
19
  expect(canonicalizeUrl(url)).toEqual('')
8
20
  })
9
21
 
10
- test('strips URLs of query strings and fragments', async () => {
11
- jest.doMock('../util/global-scope', () => ({
12
- initialLocation: 'http://different-domain.com/'
13
- }))
14
- const { canonicalizeUrl } = await import('./canonicalize-url')
15
- expect(canonicalizeUrl('http://example.com/path?query=string#fragment')).toBe('http://example.com/path')
16
- expect(canonicalizeUrl('https://www.example.com/path/to/file.html?param=value')).toBe('https://www.example.com/path/to/file.html')
17
- expect(canonicalizeUrl('https://www.example.com/?param=value#fragment')).toBe('https://www.example.com/')
22
+ test('uses cleanURL to clean the input and initial location URLs', () => {
23
+ const url = faker.internet.url()
24
+ canonicalizeUrl(url)
25
+
26
+ expect(cleanUrlModule.cleanURL).toHaveBeenCalledWith(globalScopeModule.initialLocation)
27
+ expect(cleanUrlModule.cleanURL).toHaveBeenCalledWith(url)
28
+ expect(cleanUrlModule.cleanURL).toHaveBeenCalledTimes(2)
18
29
  })
19
30
 
20
- test('returns <inline> when matching the page URL of the loader', async () => {
21
- jest.doMock('../util/global-scope', () => ({
22
- initialLocation: 'http://example.com/'
23
- }))
24
- const { canonicalizeUrl } = await import('./canonicalize-url')
25
- expect(canonicalizeUrl('http://example.com/')).toEqual('<inline>')
31
+ test('returns <inline> when input and initial page urls are the same', async () => {
32
+ expect(canonicalizeUrl(globalScopeModule.initialLocation)).toEqual('<inline>')
33
+ })
34
+
35
+ test('returns input url when it does not match initial page url', async () => {
36
+ const url = faker.internet.url()
37
+
38
+ expect(canonicalizeUrl(url)).toEqual(url)
26
39
  })
27
40
 
28
41
  test('does not identify sub-paths of the loader origin as <inline>', async () => {
29
- jest.doMock('../util/global-scope', () => ({
30
- initialLocation: 'http://example.com/'
31
- }))
32
- const { canonicalizeUrl } = await import('./canonicalize-url')
33
- expect(canonicalizeUrl('http://example.com/path/to/script.js')).not.toEqual('<inline>')
42
+ const url = globalScopeModule.initialLocation + '/path/to/script.js'
43
+
44
+ expect(canonicalizeUrl(url)).not.toEqual('<inline>')
34
45
  })
@@ -1,24 +1,29 @@
1
+ import { faker } from '@faker-js/faker'
2
+ import * as stringifyModule from './stringify'
1
3
  import { dataSize } from './data-size'
2
4
 
5
+ jest.mock('./stringify')
6
+
3
7
  describe('dataSize', () => {
4
8
  test('returns length of string', () => {
5
- const str = 'Hello, world!'
9
+ const str = faker.lorem.sentence()
6
10
  expect(dataSize(str)).toBe(str.length)
7
11
  })
8
12
 
9
13
  test('returns undefined for non-object, number, or empty string', () => {
10
14
  expect(dataSize(Infinity)).toBeUndefined()
11
- expect(dataSize(12345)).toBeUndefined() // might not actually be by design, but this is how it works today
15
+ expect(dataSize(NaN)).toBeUndefined()
16
+ expect(dataSize(12345)).toBeUndefined()
12
17
  expect(dataSize('')).toBeUndefined()
13
18
  })
14
19
 
15
20
  test('returns byte length of ArrayBuffer object', () => {
16
- const buffer = new ArrayBuffer(8)
17
- expect(dataSize(buffer)).toBe(8)
21
+ const buffer = new ArrayBuffer(faker.datatype.number({ min: 10, max: 100 }))
22
+ expect(dataSize(buffer)).toBe(buffer.byteLength)
18
23
  })
19
24
 
20
25
  test('returns size of Blob object', () => {
21
- const blob = new Blob(['Hello, world!'], { type: 'text/plain' })
26
+ const blob = new Blob([faker.lorem.sentence()], { type: 'text/plain' })
22
27
  expect(dataSize(blob)).toBe(blob.size)
23
28
  })
24
29
 
@@ -27,24 +32,26 @@ describe('dataSize', () => {
27
32
  expect(dataSize(formData)).toBeUndefined()
28
33
  })
29
34
 
30
- test('returns length of JSON string representation of object', () => {
31
- const obj = {
32
- str: 'Hello, world!',
33
- num: 12345,
34
- nestedObj: {
35
- arr: [1, 2, 3]
36
- }
35
+ test('uses stringify to get the length of an object', () => {
36
+ const input = {
37
+ [faker.datatype.uuid()]: faker.lorem.sentence()
37
38
  }
38
- const expectedSize = JSON.stringify(obj).length
39
- expect(dataSize(obj)).toBe(expectedSize)
39
+ const expectedSize = faker.datatype.number({ min: 1000, max: 10000 })
40
+
41
+ jest.spyOn(stringifyModule, 'stringify').mockReturnValue({ length: expectedSize })
42
+
43
+ expect(dataSize(input)).toBe(expectedSize)
40
44
  })
41
45
 
42
- test('returns undefined for object with toJSON method that throws an error', () => {
43
- const obj = {
44
- toJSON: () => {
45
- throw new Error('Error in toJSON')
46
- }
46
+ test('should not throw an exception if stringify throws an exception', () => {
47
+ const input = {
48
+ [faker.datatype.uuid()]: faker.lorem.sentence()
47
49
  }
48
- expect(dataSize(obj)).toBeUndefined()
50
+ const expectedSize = faker.datatype.number({ min: 1000, max: 10000 })
51
+
52
+ jest.spyOn(stringifyModule, 'stringify').mockImplementation(() => { throw new Error(faker.lorem.sentence()) })
53
+
54
+ expect(() => dataSize(input)).not.toThrow()
55
+ expect(dataSize(input)).toBeUndefined()
49
56
  })
50
57
  })
@@ -2,7 +2,6 @@
2
2
  * Copyright 2020 New Relic Corporation. All rights reserved.
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
- import { mapOwn } from './map-own'
6
5
  import { ee } from '../event-emitter/contextual-ee'
7
6
  import { handle } from '../event-emitter/handle'
8
7
  import { drain } from '../drain/drain'
@@ -13,24 +12,37 @@ const bucketMap = {
13
12
  err: [FEATURE_NAMES.jserrors, FEATURE_NAMES.metrics],
14
13
  ins: [FEATURE_NAMES.pageAction],
15
14
  spa: [FEATURE_NAMES.spa],
16
- sr: [FEATURE_NAMES.sessionReplay]
15
+ sr: [FEATURE_NAMES.sessionReplay, FEATURE_NAMES.sessionTrace]
17
16
  }
18
17
 
18
+ /** Note that this function only processes each unique flag ONCE, with the first occurrence of each flag and numeric value determining its switch on/off setting. */
19
19
  export function activateFeatures (flags, agentIdentifier) {
20
- var sharedEE = ee.get(agentIdentifier)
20
+ const sharedEE = ee.get(agentIdentifier)
21
21
  if (!(flags && typeof flags === 'object')) return
22
- mapOwn(flags, function (flag, val) {
23
- if (!val) {
24
- return (bucketMap[flag] || []).forEach(feat => {
25
- handle('block-' + flag, [], undefined, feat, sharedEE)
22
+
23
+ Object.entries(flags).forEach(([flag, num]) => {
24
+ if (activatedFeatures[flag] !== undefined) return
25
+
26
+ if (bucketMap[flag]) {
27
+ bucketMap[flag].forEach(feat => {
28
+ if (!num) handle('block-' + flag, [], undefined, feat, sharedEE)
29
+ else handle('feat-' + flag, [], undefined, feat, sharedEE)
30
+
31
+ handle('rumresp-' + flag, [Boolean(num)], undefined, feat, sharedEE) // this is a duplicate of feat-/block- but makes awaiting for 1 event easier than 2
26
32
  })
27
- }
33
+ } else if (num) handle('feat-' + flag, [], undefined, undefined, sharedEE) // not sure what other flags are overlooked, but there's a test for ones not in the map --
34
+ // ^^^ THIS DOESN'T ACTUALLY DO ANYTHHING AS UNDEFINED/FEATURE GROUP ISN'T DRAINED
35
+
36
+ activatedFeatures[flag] = Boolean(num)
37
+ })
28
38
 
29
- if (activatedFeatures[flag]) {
30
- return
39
+ // Let the features waiting on their respective flags know that RUM response was received and that any missing flags are interpreted as bad entitlement / "off".
40
+ // Hence, those features will not be hanging forever if their flags aren't included in the response.
41
+ Object.keys(bucketMap).forEach(flag => {
42
+ if (activatedFeatures[flag] === undefined) {
43
+ bucketMap[flag]?.forEach(feat => handle('rumresp-' + flag, [false], undefined, feat, sharedEE))
44
+ activatedFeatures[flag] = false
31
45
  }
32
- handle('feat-' + flag, [], undefined, bucketMap[flag], sharedEE)
33
- activatedFeatures[flag] = true
34
46
  })
35
47
  drain(agentIdentifier, FEATURE_NAMES.pageViewEvent)
36
48
  }
@@ -0,0 +1,98 @@
1
+ import { faker } from '@faker-js/faker'
2
+ import * as eventEmitterModule from '../event-emitter/contextual-ee'
3
+ import * as handleModule from '../event-emitter/handle'
4
+ import * as drainModule from '../drain/drain'
5
+ import { activateFeatures, activatedFeatures } from './feature-flags'
6
+ import { FEATURE_NAMES } from '../../loaders/features/features'
7
+
8
+ jest.mock('../event-emitter/handle')
9
+ jest.mock('../drain/drain')
10
+ jest.mock('../event-emitter/contextual-ee', () => ({
11
+ __esModule: true,
12
+ ee: {
13
+ get: jest.fn(() => ({
14
+ foo: `bar_${Math.random()}`
15
+ }))
16
+ }
17
+ }))
18
+
19
+ let agentIdentifier
20
+
21
+ beforeEach(() => {
22
+ agentIdentifier = faker.datatype.uuid()
23
+ })
24
+
25
+ afterEach(() => {
26
+ Object.keys(activatedFeatures)
27
+ .forEach(key => delete activatedFeatures[key])
28
+ })
29
+
30
+ test.each([
31
+ null,
32
+ undefined
33
+ ])('should not do anything when flags is %s', (input) => {
34
+ activateFeatures(input, agentIdentifier)
35
+
36
+ expect(handleModule.handle).not.toHaveBeenCalled()
37
+ expect(drainModule.drain).not.toHaveBeenCalled()
38
+ expect(activatedFeatures).toEqual({})
39
+ })
40
+
41
+ const bucketMap = {
42
+ stn: [FEATURE_NAMES.sessionTrace],
43
+ err: [FEATURE_NAMES.jserrors, FEATURE_NAMES.metrics],
44
+ ins: [FEATURE_NAMES.pageAction],
45
+ spa: [FEATURE_NAMES.spa],
46
+ sr: [FEATURE_NAMES.sessionReplay, FEATURE_NAMES.sessionTrace]
47
+ }
48
+
49
+ test('emits the right events when feature flag = 1', () => {
50
+ const flags = {}
51
+ Object.keys(bucketMap).forEach(flag => flags[flag] = 1)
52
+ activateFeatures(flags, agentIdentifier)
53
+
54
+ const sharedEE = jest.mocked(eventEmitterModule.ee.get).mock.results[0].value
55
+
56
+ // each flag gets emitted to each of its mapped features, and a feat- AND a rumresp- for every emit, so (1+2+1+1+2)*2 = 14
57
+ expect(handleModule.handle).toHaveBeenCalledTimes(14)
58
+ expect(handleModule.handle).toHaveBeenNthCalledWith(1, 'feat-stn', [], undefined, FEATURE_NAMES.sessionTrace, sharedEE)
59
+ expect(handleModule.handle).toHaveBeenLastCalledWith('rumresp-sr', [true], undefined, FEATURE_NAMES.sessionTrace, sharedEE)
60
+ expect(drainModule.drain).toHaveBeenCalledWith(agentIdentifier, 'page_view_event')
61
+
62
+ Object.keys(flags).forEach(flag => flags[flag] = true)
63
+ expect(activatedFeatures).toEqual(flags)
64
+ })
65
+
66
+ test('emits the right events when feature flag = 0', () => {
67
+ const flags = {}
68
+ Object.keys(bucketMap).forEach(flag => flags[flag] = 0)
69
+ activateFeatures(flags, agentIdentifier)
70
+
71
+ const sharedEE = jest.mocked(eventEmitterModule.ee.get).mock.results[0].value
72
+
73
+ // each flag gets emitted to each of its mapped features, and a block- AND a rumresp- for every emit, so (1+2+1+1+2)*2 = 14
74
+ expect(handleModule.handle).toHaveBeenCalledTimes(14)
75
+ expect(handleModule.handle).toHaveBeenNthCalledWith(1, 'block-stn', [], undefined, FEATURE_NAMES.sessionTrace, sharedEE)
76
+ expect(handleModule.handle).toHaveBeenLastCalledWith('rumresp-sr', [false], undefined, FEATURE_NAMES.sessionTrace, sharedEE)
77
+ expect(drainModule.drain).toHaveBeenCalledWith(agentIdentifier, 'page_view_event')
78
+
79
+ Object.keys(flags).forEach(flag => flags[flag] = false)
80
+ expect(activatedFeatures).toEqual(flags)
81
+ })
82
+
83
+ test('only the first activate of the same feature is respected', () => {
84
+ const flags = { stn: 1 }
85
+ activateFeatures(flags, agentIdentifier)
86
+ flags.stn = 0
87
+ activateFeatures(flags, agentIdentifier)
88
+
89
+ const sharedEE1 = jest.mocked(eventEmitterModule.ee.get).mock.results[0].value
90
+ const sharedEE2 = jest.mocked(eventEmitterModule.ee.get).mock.results[1].value
91
+
92
+ expect(handleModule.handle).toHaveBeenNthCalledWith(1, 'feat-stn', [], undefined, 'session_trace', sharedEE1)
93
+ expect(handleModule.handle).toHaveBeenNthCalledWith(2, 'rumresp-stn', [true], undefined, 'session_trace', sharedEE1)
94
+ expect(handleModule.handle).not.toHaveBeenNthCalledWith(1, 'feat-stn', [], undefined, 'session_trace', sharedEE2)
95
+ expect(drainModule.drain).toHaveBeenCalledWith(agentIdentifier, 'page_view_event')
96
+ expect(drainModule.drain).toHaveBeenCalledTimes(2)
97
+ expect(activatedFeatures.stn).toBeTruthy()
98
+ })
@@ -21,29 +21,3 @@ export let globalScope = (() => {
21
21
  })()
22
22
 
23
23
  export const initialLocation = '' + globalScope.location
24
-
25
- /**
26
- * The below methods are only used for testing and should be removed once the
27
- * reliant tests are moved to Jest.
28
- * tests/browser/protocol.browser.js
29
- * tests/browser/obfuscate.browser.js
30
- */
31
- export function setScope (obj) {
32
- globalScope = { ...obj }
33
- }
34
- export function resetScope () {
35
- if (isBrowserScope) {
36
- globalScope = window
37
- } else if (isWorkerScope) {
38
- if (typeof globalThis !== 'undefined' && globalThis instanceof WorkerGlobalScope) {
39
- globalScope = globalThis
40
- } else if (self instanceof WorkerGlobalScope) {
41
- globalScope = self
42
- }
43
- } else {
44
- throw new Error('New Relic browser agent shutting down due to error: Unable to locate global scope. This is possibly due to code redefining browser global variables like "self" and "window".')
45
- }
46
- }
47
- export function getGlobalScope () {
48
- return globalScope
49
- }
@@ -0,0 +1,87 @@
1
+ /*
2
+ The global-scope module contains exports that are defined once at the time
3
+ of importing the module. For this reason, the module must be dynamically
4
+ imported in each test case.
5
+
6
+ A scope must always exist or importing the module will throw an error. Use
7
+ `enableWorkerScope` to enable the worker scope. Be sure to call `disableWorkerScope`
8
+ before any calls to `expect` or the test will fail with an error from Jest.
9
+ */
10
+
11
+ import { faker } from '@faker-js/faker'
12
+
13
+ afterEach(() => {
14
+ jest.restoreAllMocks()
15
+ jest.resetModules()
16
+ })
17
+
18
+ test('should indicate a browser scope', async () => {
19
+ jest.spyOn(global, 'window', 'get').mockReturnValue({ document: {} })
20
+
21
+ const globalScopeModule = await import('./global-scope')
22
+
23
+ expect(globalScopeModule.isBrowserScope).toBe(true)
24
+ expect(globalScopeModule.isWorkerScope).toBe(false)
25
+ expect(globalScopeModule.globalScope).toBe(global.window)
26
+ })
27
+
28
+ test('should indicate a worker scope', async () => {
29
+ enableWorkerScope()
30
+ const globalScopeModule = await import('./global-scope')
31
+ const mockedGlobalThis = global.globalThis
32
+ disableWorkerScope()
33
+
34
+ expect(globalScopeModule.isBrowserScope).toBe(false)
35
+ expect(globalScopeModule.isWorkerScope).toBe(true)
36
+ expect(globalScopeModule.globalScope).toBe(mockedGlobalThis)
37
+ })
38
+
39
+ test('should return the self global', async () => {
40
+ enableWorkerScope()
41
+ jest.replaceProperty(global, 'globalThis', null)
42
+ jest.spyOn(global, 'self', 'get').mockReturnValue(new global.WorkerGlobalScope())
43
+
44
+ const globalScopeModule = await import('./global-scope')
45
+ const mockedGlobalSelf = global.self
46
+ disableWorkerScope()
47
+
48
+ expect(globalScopeModule.isBrowserScope).toBe(false)
49
+ expect(globalScopeModule.isWorkerScope).toBe(true)
50
+ expect(globalScopeModule.globalScope).toBe(mockedGlobalSelf)
51
+ })
52
+
53
+ test('should throw an error when a scope cannot be defined', async () => {
54
+ jest.spyOn(global, 'window', 'get').mockReturnValue(undefined)
55
+
56
+ await expect(() => import('./global-scope')).rejects.toThrow()
57
+ })
58
+
59
+ test('should immediately store the current location', async () => {
60
+ const url = faker.internet.url()
61
+ jest.spyOn(window, 'location', 'get').mockReturnValue(url)
62
+
63
+ const globalScopeModule = await import('./global-scope')
64
+
65
+ expect(globalScopeModule.initialLocation).toBe(url)
66
+ })
67
+
68
+ function enableWorkerScope () {
69
+ jest.spyOn(global, 'window', 'get').mockReturnValue(undefined)
70
+
71
+ class WorkerNavigator {}
72
+ class WorkerGlobalScope {
73
+ navigator = new WorkerNavigator()
74
+ }
75
+ global.WorkerGlobalScope = WorkerGlobalScope
76
+ global.WorkerNavigator = WorkerNavigator
77
+
78
+ jest.spyOn(global, 'navigator', 'get').mockReturnValue(new global.WorkerNavigator())
79
+ jest.replaceProperty(global, 'globalThis', new WorkerGlobalScope())
80
+ }
81
+
82
+ function disableWorkerScope () {
83
+ delete global.WorkerGlobalScope
84
+ delete global.WorkerNavigator
85
+
86
+ jest.restoreAllMocks()
87
+ }
@@ -0,0 +1,173 @@
1
+ import { faker } from '@faker-js/faker'
2
+ import * as configModule from '../config/config'
3
+ import * as urlProtocolModule from '../url/protocol'
4
+ import * as consolModule from './console'
5
+ import * as obfuscateModule from './obfuscate'
6
+
7
+ jest.mock('../config/config')
8
+ jest.mock('../context/shared-context')
9
+ jest.mock('../url/protocol')
10
+ jest.mock('./console')
11
+
12
+ let agentIdentifier
13
+ const rules = [{
14
+ regex: /pii/g,
15
+ replacement: 'OBFUSCATED'
16
+ }]
17
+
18
+ beforeEach(() => {
19
+ agentIdentifier = faker.datatype.uuid()
20
+ })
21
+
22
+ afterEach(() => {
23
+ jest.resetAllMocks()
24
+ })
25
+
26
+ describe('Obfuscator', () => {
27
+ test('shouldObfuscate returns true when there are rules', () => {
28
+ jest.spyOn(configModule, 'getConfigurationValue').mockReturnValue(rules)
29
+
30
+ const obfuscator = new obfuscateModule.Obfuscator()
31
+ obfuscator.sharedContext = { agentIdentifier }
32
+
33
+ expect(obfuscator.shouldObfuscate()).toEqual(true)
34
+ })
35
+
36
+ test('shouldObfuscate returns false when there are no rules', () => {
37
+ jest.spyOn(configModule, 'getConfigurationValue').mockReturnValue([])
38
+
39
+ const obfuscator = new obfuscateModule.Obfuscator()
40
+ obfuscator.sharedContext = { agentIdentifier }
41
+
42
+ expect(obfuscator.shouldObfuscate()).toEqual(false)
43
+ })
44
+
45
+ test('obfuscateString returns the input when there are no rules', () => {
46
+ jest.spyOn(configModule, 'getConfigurationValue').mockReturnValue([])
47
+
48
+ const input = faker.lorem.sentence()
49
+ const obfuscator = new obfuscateModule.Obfuscator()
50
+ obfuscator.sharedContext = { agentIdentifier }
51
+
52
+ expect(obfuscator.obfuscateString(input)).toEqual(input)
53
+ })
54
+
55
+ test('obfuscateString applies obfuscation rules to input', () => {
56
+ jest.spyOn(configModule, 'getConfigurationValue').mockReturnValue(rules)
57
+
58
+ const input = 'pii'
59
+ const obfuscator = new obfuscateModule.Obfuscator()
60
+ obfuscator.sharedContext = { agentIdentifier }
61
+
62
+ expect(obfuscator.obfuscateString(input)).toEqual(rules[0].replacement)
63
+ })
64
+
65
+ test('obfuscateString replaces input with * when replacement is not set', () => {
66
+ const newRules = [{
67
+ regex: rules[0].regex
68
+ }]
69
+
70
+ jest.spyOn(configModule, 'getConfigurationValue').mockReturnValue(newRules)
71
+
72
+ const input = 'pii'
73
+ const obfuscator = new obfuscateModule.Obfuscator()
74
+ obfuscator.sharedContext = { agentIdentifier }
75
+
76
+ expect(obfuscator.obfuscateString(input)).toEqual('*')
77
+ })
78
+
79
+ test.each([
80
+ null,
81
+ undefined,
82
+ '',
83
+ 123
84
+ ])('obfuscateString returns the input it is %s', (input) => {
85
+ jest.spyOn(configModule, 'getConfigurationValue').mockReturnValue(rules)
86
+
87
+ const obfuscator = new obfuscateModule.Obfuscator()
88
+ obfuscator.sharedContext = { agentIdentifier }
89
+
90
+ expect(obfuscator.obfuscateString(input)).toEqual(input)
91
+ })
92
+ })
93
+
94
+ describe('getRules', () => {
95
+ test('should return configured rules', () => {
96
+ jest.spyOn(configModule, 'getConfigurationValue').mockReturnValue(rules)
97
+
98
+ expect(obfuscateModule.getRules()).toEqual(rules)
99
+ })
100
+
101
+ test('should include the file protocol obfuscation', () => {
102
+ jest.spyOn(configModule, 'getConfigurationValue').mockReturnValue(rules)
103
+ jest.spyOn(urlProtocolModule, 'isFileProtocol').mockReturnValue(rules)
104
+
105
+ expect(obfuscateModule.getRules()).toEqual(expect.arrayContaining([{
106
+ regex: /^file:\/\/(.*)/,
107
+ replacement: 'file://OBFUSCATED'
108
+ }]))
109
+ })
110
+
111
+ test.each([
112
+ null,
113
+ undefined
114
+ ])('should return an empty array when obfuscation rules are %s', (input) => {
115
+ jest.spyOn(configModule, 'getConfigurationValue').mockReturnValue(input)
116
+
117
+ expect(obfuscateModule.getRules()).toEqual([])
118
+ })
119
+ })
120
+
121
+ describe('validateRules', () => {
122
+ test('should return true for empty array', () => {
123
+ expect(obfuscateModule.validateRules([])).toEqual(true)
124
+ })
125
+
126
+ test('should return true for valid rules', () => {
127
+ expect(obfuscateModule.validateRules(rules)).toEqual(true)
128
+ })
129
+
130
+ test.each([
131
+ null,
132
+ 123,
133
+ {},
134
+ []
135
+ ])('should warn about an invalid regex type %s', (input) => {
136
+ const newRules = [{
137
+ regex: input,
138
+ replacement: rules[0].replacement
139
+ }]
140
+
141
+ expect(obfuscateModule.validateRules(newRules)).toEqual(false)
142
+ expect(consolModule.warn).toHaveBeenCalledWith(expect.stringContaining(
143
+ 'contains a "regex" value with an invalid type'
144
+ ))
145
+ })
146
+
147
+ test('should warn about a missing regex with value', () => {
148
+ const newRules = [{
149
+ replacement: rules[0].replacement
150
+ }]
151
+
152
+ expect(obfuscateModule.validateRules(newRules)).toEqual(false)
153
+ expect(consolModule.warn).toHaveBeenCalledWith(expect.stringContaining(
154
+ 'missing a "regex" value'
155
+ ))
156
+ })
157
+
158
+ test.each([
159
+ 123,
160
+ {},
161
+ []
162
+ ])('should warn about an invalid replacement type %s', (input) => {
163
+ const newRules = [{
164
+ regex: rules[0].regex,
165
+ replacement: input
166
+ }]
167
+
168
+ expect(obfuscateModule.validateRules(newRules)).toEqual(false)
169
+ expect(consolModule.warn).toHaveBeenCalledWith(expect.stringContaining(
170
+ 'contains a "replacement" value with an invalid type'
171
+ ))
172
+ })
173
+ })
@@ -5,7 +5,7 @@ import { warn } from './console'
5
5
 
6
6
  var fileProtocolRule = {
7
7
  regex: /^file:\/\/(.*)/,
8
- replacement: 'file://OBFUSCATED'
8
+ replacement: atob('ZmlsZTovL09CRlVTQ0FURUQ=')
9
9
  }
10
10
  export class Obfuscator extends SharedContext {
11
11
  shouldObfuscate () {
@@ -51,7 +51,7 @@ export function validateRules (rules) {
51
51
  if (!('regex' in rules[i])) {
52
52
  warn('An obfuscation replacement rule was detected missing a "regex" value.')
53
53
  invalidRegexDetected = true
54
- } else if (typeof rules[i].regex !== 'string' && !(rules[i].regex.constructor === RegExp)) {
54
+ } else if (typeof rules[i].regex !== 'string' && !(rules[i].regex instanceof RegExp)) {
55
55
  warn('An obfuscation replacement rule contains a "regex" value with an invalid type (must be a string or RegExp)')
56
56
  invalidRegexDetected = true
57
57
  }
@@ -97,10 +97,10 @@ submitData.img = function img ({ url }) {
97
97
  * @returns {boolean}
98
98
  */
99
99
  submitData.beacon = function ({ url, body }) {
100
- // Navigator has to be bound to ensure it does not error in some browsers
101
- // https://xgwang.me/posts/you-may-not-know-beacon/#it-may-throw-error%2C-be-sure-to-catch
102
- const send = window.navigator.sendBeacon.bind(window.navigator)
103
100
  try {
101
+ // Navigator has to be bound to ensure it does not error in some browsers
102
+ // https://xgwang.me/posts/you-may-not-know-beacon/#it-may-throw-error%2C-be-sure-to-catch
103
+ const send = window.navigator.sendBeacon.bind(window.navigator)
104
104
  return send(url, body)
105
105
  } catch (err) {
106
106
  // if sendBeacon still trys to throw an illegal invocation error,