@newrelic/browser-agent 1.233.0 → 1.234.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 (115) hide show
  1. package/dist/cjs/cdn/experimental.js +27 -0
  2. package/dist/cjs/common/config/state/init.js +1 -1
  3. package/dist/cjs/common/constants/env.cdn.js +1 -1
  4. package/dist/cjs/common/constants/env.npm.js +1 -1
  5. package/dist/cjs/common/event-emitter/contextual-ee.test.js +10 -10
  6. package/dist/cjs/common/harvest/harvest-scheduler.js +18 -3
  7. package/dist/cjs/common/harvest/harvest-scheduler.test.js +39 -0
  8. package/dist/cjs/common/harvest/harvest.js +14 -34
  9. package/dist/cjs/common/harvest/harvest.test.js +224 -0
  10. package/dist/cjs/common/url/encode.js +2 -2
  11. package/dist/cjs/common/util/console.test.js +30 -0
  12. package/dist/cjs/common/util/get-or-set.js +8 -1
  13. package/dist/cjs/common/util/get-or-set.test.js +47 -0
  14. package/dist/cjs/common/util/stringify.test.js +48 -0
  15. package/dist/cjs/common/util/submit-data.js +15 -15
  16. package/dist/cjs/common/util/submit-data.test.js +221 -0
  17. package/dist/cjs/common/util/traverse.js +19 -27
  18. package/dist/cjs/common/util/traverse.test.js +44 -0
  19. package/dist/cjs/features/metrics/aggregate/endpoint-map.js +14 -0
  20. package/dist/cjs/features/metrics/aggregate/index.js +3 -2
  21. package/dist/cjs/features/metrics/instrument/index.js +0 -2
  22. package/dist/cjs/features/page_view_event/aggregate/index.js +58 -44
  23. package/dist/cjs/features/session_replay/aggregate/index.js +10 -7
  24. package/dist/cjs/loaders/configure/configure.js +0 -1
  25. package/dist/esm/cdn/experimental.js +24 -0
  26. package/dist/esm/common/config/state/init.js +1 -1
  27. package/dist/esm/common/constants/env.cdn.js +1 -1
  28. package/dist/esm/common/constants/env.npm.js +1 -1
  29. package/dist/esm/common/event-emitter/contextual-ee.test.js +10 -10
  30. package/dist/esm/common/harvest/harvest-scheduler.js +18 -3
  31. package/dist/esm/common/harvest/harvest-scheduler.test.js +37 -0
  32. package/dist/esm/common/harvest/harvest.js +14 -34
  33. package/dist/esm/common/harvest/harvest.test.js +222 -0
  34. package/dist/esm/common/url/encode.js +2 -2
  35. package/dist/esm/common/util/console.test.js +28 -0
  36. package/dist/esm/common/util/get-or-set.js +8 -1
  37. package/dist/esm/common/util/get-or-set.test.js +45 -0
  38. package/dist/esm/common/util/stringify.test.js +46 -0
  39. package/dist/esm/common/util/submit-data.js +15 -15
  40. package/dist/esm/common/util/submit-data.test.js +219 -0
  41. package/dist/esm/common/util/traverse.js +19 -27
  42. package/dist/esm/common/util/traverse.test.js +42 -0
  43. package/dist/esm/features/metrics/aggregate/endpoint-map.js +7 -0
  44. package/dist/esm/features/metrics/aggregate/index.js +3 -2
  45. package/dist/esm/features/metrics/instrument/index.js +0 -2
  46. package/dist/esm/features/page_view_event/aggregate/index.js +58 -44
  47. package/dist/esm/features/session_replay/aggregate/index.js +10 -7
  48. package/dist/esm/loaders/configure/configure.js +0 -1
  49. package/dist/types/cdn/experimental.d.ts +2 -0
  50. package/dist/types/cdn/experimental.d.ts.map +1 -0
  51. package/dist/types/common/config/state/init.d.ts.map +1 -1
  52. package/dist/types/common/context/shared-context.d.ts.map +1 -1
  53. package/dist/types/common/harvest/harvest-scheduler.d.ts +26 -3
  54. package/dist/types/common/harvest/harvest-scheduler.d.ts.map +1 -1
  55. package/dist/types/common/harvest/harvest.d.ts +2 -2
  56. package/dist/types/common/harvest/harvest.d.ts.map +1 -1
  57. package/dist/types/common/timer/timer.d.ts.map +1 -1
  58. package/dist/types/common/util/get-or-set.d.ts +9 -1
  59. package/dist/types/common/util/get-or-set.d.ts.map +1 -1
  60. package/dist/types/common/util/submit-data.d.ts +14 -10
  61. package/dist/types/common/util/submit-data.d.ts.map +1 -1
  62. package/dist/types/common/util/traverse.d.ts +10 -1
  63. package/dist/types/common/util/traverse.d.ts.map +1 -1
  64. package/dist/types/common/window/nreum.d.ts.map +1 -1
  65. package/dist/types/features/metrics/aggregate/endpoint-map.d.ts +8 -0
  66. package/dist/types/features/metrics/aggregate/endpoint-map.d.ts.map +1 -0
  67. package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
  68. package/dist/types/features/metrics/aggregate/polyfill-detection.es5.d.ts.map +1 -1
  69. package/dist/types/features/metrics/instrument/index.d.ts.map +1 -1
  70. package/dist/types/features/page_view_event/aggregate/index.d.ts.map +1 -1
  71. package/dist/types/features/page_view_event/instrument/index.d.ts.map +1 -1
  72. package/dist/types/features/session_replay/aggregate/index.d.ts +4 -0
  73. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  74. package/dist/types/features/utils/handler-cache.d.ts.map +1 -1
  75. package/dist/types/loaders/configure/configure.d.ts.map +1 -1
  76. package/package.json +8 -3
  77. package/src/cdn/experimental.js +36 -0
  78. package/src/common/config/state/init.js +1 -2
  79. package/src/common/context/shared-context.js +0 -1
  80. package/src/common/event-emitter/contextual-ee.test.js +10 -10
  81. package/src/common/harvest/harvest-scheduler.js +18 -3
  82. package/src/common/harvest/harvest-scheduler.test.js +25 -0
  83. package/src/common/harvest/harvest.js +15 -20
  84. package/src/common/harvest/harvest.test.js +169 -0
  85. package/src/common/session/session-entity.test.js +0 -1
  86. package/src/common/timer/timer.js +0 -1
  87. package/src/common/url/encode.js +2 -2
  88. package/src/common/util/console.test.js +34 -0
  89. package/src/common/util/get-or-set.js +8 -1
  90. package/src/common/util/get-or-set.test.js +58 -0
  91. package/src/common/util/stringify.test.js +49 -0
  92. package/src/common/util/submit-data.js +15 -16
  93. package/src/common/util/submit-data.test.js +218 -0
  94. package/src/common/util/traverse.js +18 -27
  95. package/src/common/util/traverse.test.js +50 -0
  96. package/src/common/window/nreum.js +0 -1
  97. package/src/features/metrics/aggregate/endpoint-map.js +7 -0
  98. package/src/features/metrics/aggregate/index.js +3 -2
  99. package/src/features/metrics/aggregate/polyfill-detection.es5.js +0 -1
  100. package/src/features/metrics/instrument/index.js +0 -2
  101. package/src/features/page_view_event/aggregate/index.js +48 -51
  102. package/src/features/page_view_event/instrument/index.js +0 -1
  103. package/src/features/session_replay/aggregate/index.js +12 -7
  104. package/src/features/utils/handler-cache.js +0 -1
  105. package/src/loaders/configure/configure.js +0 -1
  106. package/dist/cjs/common/util/s-hash.js +0 -19
  107. package/dist/cjs/features/metrics/instrument/workers-helper.js +0 -124
  108. package/dist/esm/common/util/s-hash.js +0 -13
  109. package/dist/esm/features/metrics/instrument/workers-helper.js +0 -119
  110. package/dist/types/common/util/s-hash.d.ts +0 -2
  111. package/dist/types/common/util/s-hash.d.ts.map +0 -1
  112. package/dist/types/features/metrics/instrument/workers-helper.d.ts +0 -7
  113. package/dist/types/features/metrics/instrument/workers-helper.d.ts.map +0 -1
  114. package/src/common/util/s-hash.js +0 -14
  115. package/src/features/metrics/instrument/workers-helper.js +0 -113
@@ -101,7 +101,7 @@ describe('event-emitter context', () => {
101
101
  })
102
102
 
103
103
  describe('event-emitter buffer', () => {
104
- it('it should create a new buffer for the given group', async () => {
104
+ test('it should create a new buffer for the given group', async () => {
105
105
  const { ee } = await import('./contextual-ee')
106
106
  const eventType = faker.datatype.uuid()
107
107
  const group = faker.datatype.uuid()
@@ -114,7 +114,7 @@ describe('event-emitter buffer', () => {
114
114
  expect(ee.isBuffering(eventType)).toEqual(true)
115
115
  })
116
116
 
117
- it('it should default group to "feature"', async () => {
117
+ test('it should default group to "feature"', async () => {
118
118
  const { ee } = await import('./contextual-ee')
119
119
  const eventType = faker.datatype.uuid()
120
120
 
@@ -126,7 +126,7 @@ describe('event-emitter buffer', () => {
126
126
  expect(ee.isBuffering(eventType)).toEqual(true)
127
127
  })
128
128
 
129
- it('it should not create buffer if event-emitter is aborted', async () => {
129
+ test('it should not create buffer if event-emitter is aborted', async () => {
130
130
  const { ee } = await import('./contextual-ee')
131
131
  const eventType = faker.datatype.uuid()
132
132
  const group = faker.datatype.uuid()
@@ -169,7 +169,7 @@ describe('event-emitter abort', () => {
169
169
  })
170
170
 
171
171
  describe('event-emitter emit', () => {
172
- it('should execute the listener', async () => {
172
+ test('should execute the listener', async () => {
173
173
  const { ee } = await import('./contextual-ee')
174
174
  const mockListener = jest.fn()
175
175
  const eventType = faker.datatype.uuid()
@@ -181,7 +181,7 @@ describe('event-emitter emit', () => {
181
181
  expect(mockListener).toHaveBeenCalledWith(eventArgs[0], eventArgs[1], eventArgs[2])
182
182
  })
183
183
 
184
- it('should not execute the listener after removal', async () => {
184
+ test('should not execute the listener after removal', async () => {
185
185
  const { ee } = await import('./contextual-ee')
186
186
  const mockListener = jest.fn()
187
187
  const eventType = faker.datatype.uuid()
@@ -195,7 +195,7 @@ describe('event-emitter emit', () => {
195
195
  expect(mockListener).toHaveBeenCalledTimes(1)
196
196
  })
197
197
 
198
- it('should return early if global event-emitter is aborted', async () => {
198
+ test('should return early if global event-emitter is aborted', async () => {
199
199
  const { ee } = await import('./contextual-ee')
200
200
  const mockListener = jest.fn()
201
201
  const eventType = faker.datatype.uuid()
@@ -211,7 +211,7 @@ describe('event-emitter emit', () => {
211
211
  expect(mockListener).toHaveBeenCalledTimes(0)
212
212
  })
213
213
 
214
- it('should still emit if global event-emitter is aborted but force flag is true', async () => {
214
+ test('should still emit if global event-emitter is aborted but force flag is true', async () => {
215
215
  const { ee } = await import('./contextual-ee')
216
216
  const scopeEE = ee.get(faker.datatype.uuid())
217
217
  const mockScopeListener = jest.fn()
@@ -228,7 +228,7 @@ describe('event-emitter emit', () => {
228
228
  expect(mockScopeListener).toHaveBeenCalledTimes(1)
229
229
  })
230
230
 
231
- it('should bubble the event if bubble flag is true', async () => {
231
+ test('should bubble the event if bubble flag is true', async () => {
232
232
  const { ee } = await import('./contextual-ee')
233
233
  const scopeEE = ee.get(faker.datatype.uuid())
234
234
  const mockListener = jest.fn()
@@ -244,7 +244,7 @@ describe('event-emitter emit', () => {
244
244
  expect(mockListener).toHaveBeenCalledTimes(1)
245
245
  })
246
246
 
247
- it('should not bubble the event if bubble flag is false', async () => {
247
+ test('should not bubble the event if bubble flag is false', async () => {
248
248
  const { ee } = await import('./contextual-ee')
249
249
  const scopeEE = ee.get(faker.datatype.uuid())
250
250
  const mockListener = jest.fn()
@@ -260,7 +260,7 @@ describe('event-emitter emit', () => {
260
260
  expect(mockListener).not.toHaveBeenCalled()
261
261
  })
262
262
 
263
- it('should buffer the event on the scoped event-emitter', async () => {
263
+ test('should buffer the event on the scoped event-emitter', async () => {
264
264
  const { ee } = await import('./contextual-ee')
265
265
  const scopeEE = ee.get(faker.datatype.uuid())
266
266
  const mockListener = jest.fn()
@@ -13,6 +13,17 @@ import { getConfigurationValue } from '../config/config'
13
13
  * Periodically invokes harvest calls and handles retries
14
14
  */
15
15
  export class HarvestScheduler extends SharedContext {
16
+ /**
17
+ * Create a HarvestScheduler
18
+ * @param {string} endpoint - The base BAM endpoint name -- ex. 'events'
19
+ * @param {object} opts - The options used to configure the HarvestScheduler
20
+ * @param {Function} opts.onFinished - The callback to be fired when a harvest has finished
21
+ * @param {Function} opts.getPayload - A callback which can be triggered to return a payload for harvesting
22
+ * @param {number} opts.retryDelay - The number of seconds to wait before retrying after a network failure
23
+ * @param {boolean} opts.raw - Use a prefabricated payload shape as the harvest payload without the need for formatting
24
+ * @param {string} opts.customUrl - A custom url that falls outside of the shape of the standard BAM harvester url pattern. Will use directly instead of concatenating various pieces
25
+ * @param {*} parent - The parent object, whose state can be passed into SharedContext
26
+ */
16
27
  constructor (endpoint, opts, parent) {
17
28
  super(parent) // gets any allowed properties from the parent and stores them in `sharedContext`
18
29
  this.endpoint = endpoint
@@ -78,7 +89,12 @@ export class HarvestScheduler extends SharedContext {
78
89
  const retry = submitMethod.method === submitData.xhr
79
90
  var payload = this.opts.getPayload({ retry: retry })
80
91
 
81
- if (!payload) return
92
+ if (!payload) {
93
+ if (this.started) {
94
+ this.scheduleHarvest()
95
+ }
96
+ return
97
+ }
82
98
 
83
99
  payload = Object.prototype.toString.call(payload) === '[object Array]' ? payload : [payload]
84
100
  harvests.push(...payload)
@@ -103,9 +119,8 @@ export class HarvestScheduler extends SharedContext {
103
119
  opts,
104
120
  submitMethod,
105
121
  cbFinished: onHarvestFinished,
106
- includeBaseParams: this.opts.includeBaseParams,
107
122
  customUrl: this.opts.customUrl,
108
- gzip: this.opts.gzip
123
+ raw: this.opts.raw
109
124
  })
110
125
  })
111
126
 
@@ -0,0 +1,25 @@
1
+ import { setConfiguration } from '../config/state/init'
2
+ import { HarvestScheduler } from './harvest-scheduler'
3
+
4
+ describe('runHarvest', () => {
5
+ test('should re-schedule harvest even if there is no accumulated data', () => {
6
+ setConfiguration('asdf', {})
7
+ const scheduler = new HarvestScheduler('events', { getPayload: jest.fn() }, { agentIdentifier: 'asdf', ee: { on: jest.fn() } })
8
+ scheduler.started = true
9
+ jest.spyOn(scheduler, 'scheduleHarvest')
10
+ scheduler.runHarvest()
11
+ expect(scheduler.opts.getPayload()).toBeFalsy()
12
+ expect(scheduler.scheduleHarvest).toHaveBeenCalledTimes(1)
13
+ })
14
+
15
+ test('should also re-schedule harvest if there is accumulated data', () => {
16
+ setConfiguration('asdf', {})
17
+ const scheduler = new HarvestScheduler('events', { getPayload: jest.fn().mockImplementation(() => 'payload') }, { agentIdentifier: 'asdf', ee: { on: jest.fn() } })
18
+ scheduler.started = true
19
+ scheduler.harvest._send = () => {}
20
+ jest.spyOn(scheduler, 'scheduleHarvest')
21
+ scheduler.runHarvest()
22
+ expect(scheduler.opts.getPayload()).toBeTruthy()
23
+ expect(scheduler.scheduleHarvest).toHaveBeenCalledTimes(1)
24
+ })
25
+ })
@@ -92,23 +92,26 @@ export class Harvest extends SharedContext {
92
92
  return this._send({ ...spec, payload })
93
93
  }
94
94
 
95
- _send ({ endpoint, payload = {}, opts = {}, submitMethod, cbFinished, customUrl, gzip, includeBaseParams = true }) {
95
+ _send ({ endpoint, payload = {}, opts = {}, submitMethod, cbFinished, customUrl, raw, includeBaseParams = true }) {
96
96
  var info = getInfo(this.sharedContext.agentIdentifier)
97
97
  if (!info.errorBeacon) return false
98
98
 
99
99
  var agentRuntime = getRuntime(this.sharedContext.agentIdentifier)
100
100
 
101
- if (!payload.body) { // no payload body? nothing to send, just run onfinish stuff and return
101
+ if (!payload.body && !opts?.sendEmptyBody) { // no payload body? nothing to send, just run onfinish stuff and return
102
102
  if (cbFinished) {
103
103
  cbFinished({ sent: false })
104
104
  }
105
105
  return false
106
106
  }
107
107
 
108
- var url = customUrl || this.getScheme() + '://' + info.errorBeacon + '/' + endpoint + '/1/' + info.licenseKey + '?'
108
+ let url = ''
109
+ if (customUrl) url = customUrl
110
+ else if (raw) url = `${this.getScheme()}://${info.errorBeacon}/${endpoint}`
111
+ else url = `${this.getScheme()}://${info.errorBeacon}${endpoint !== 'rum' ? `/${endpoint}` : ''}/1/${info.licenseKey}`
109
112
 
110
- var baseParams = includeBaseParams ? this.baseQueryString() : ''
111
- var params = payload.qs ? encodeObj(payload.qs, agentRuntime.maxBytes) : ''
113
+ var baseParams = !raw && includeBaseParams ? this.baseQueryString() : ''
114
+ var payloadParams = payload.qs ? encodeObj(payload.qs, agentRuntime.maxBytes) : ''
112
115
  if (!submitMethod) {
113
116
  submitMethod = getSubmitMethod(endpoint, opts)
114
117
  }
@@ -116,7 +119,9 @@ export class Harvest extends SharedContext {
116
119
  var useBody = submitMethod.useBody
117
120
 
118
121
  var body
119
- var fullUrl = url + baseParams + params
122
+ var fullUrl = `${url}?${baseParams}${payloadParams}`
123
+
124
+ const gzip = payload?.qs?.content_encoding === 'gzip'
120
125
 
121
126
  if (!gzip) {
122
127
  if (useBody && endpoint === 'events') {
@@ -135,13 +140,7 @@ export class Harvest extends SharedContext {
135
140
 
136
141
  const headers = []
137
142
 
138
- if (gzip) {
139
- headers.push({ key: 'content-type', value: 'application/json' })
140
- headers.push({ key: 'X-Browser-Monitoring-Key', value: info.licenseKey })
141
- headers.push({ key: 'Content-Encoding', value: 'gzip' })
142
- } else {
143
- headers.push({ key: 'content-type', value: 'text/plain' })
144
- }
143
+ headers.push({ key: 'content-type', value: 'text/plain' })
145
144
 
146
145
  /* Since workers don't support sendBeacon right now, or Image(), they can only use XHR method.
147
146
  Because they still do permit synch XHR, the idea is that at final harvest time (worker is closing),
@@ -209,6 +208,7 @@ export class Harvest extends SharedContext {
209
208
  if (singlePayload.body) mapOwn(singlePayload.body, makeBody)
210
209
  if (singlePayload.qs) mapOwn(singlePayload.qs, makeQueryString)
211
210
  }
211
+
212
212
  return { body: makeBody(), qs: makeQueryString() }
213
213
  }
214
214
 
@@ -224,17 +224,12 @@ export class Harvest extends SharedContext {
224
224
  }
225
225
  }
226
226
 
227
- function or (a, b) { return a || b }
228
-
229
227
  export function getSubmitMethod (endpoint, opts) {
230
228
  opts = opts || {}
231
229
  var method
232
230
  var useBody
233
231
 
234
- if (opts.needResponse) { // currently: only STN needs a response
235
- useBody = true
236
- method = submitData.xhr
237
- } else if (opts.unload && isBrowserScope) { // all the features' final harvest; neither methods work outside window context
232
+ if (opts.unload && isBrowserScope) { // all the features' final harvest; neither methods work outside window context
238
233
  useBody = haveSendBeacon
239
234
  method = haveSendBeacon ? submitData.beacon : submitData.img // really only IE doesn't have Beacon API for web browsers
240
235
  } else {
@@ -264,7 +259,7 @@ function createAccumulator () {
264
259
  var accumulator = {}
265
260
  var hasData = false
266
261
  return function (key, val) {
267
- if (val !== null && val !== undefined && val.length) {
262
+ if (val !== null && val !== undefined && val.toString()?.length) {
268
263
  accumulator[key] = val
269
264
  hasData = true
270
265
  }
@@ -0,0 +1,169 @@
1
+ import { submitData } from '../util/submit-data'
2
+ import { Harvest } from './harvest'
3
+
4
+ jest.mock('../context/shared-context', () => ({
5
+ __esModule: true,
6
+ SharedContext: function () {
7
+ this.sharedContext = {
8
+ agentIdentifier: 'abcd'
9
+ }
10
+ }
11
+ }))
12
+ jest.mock('../config/config', () => ({
13
+ __esModule: true,
14
+ getConfigurationValue: jest.fn(),
15
+ getInfo: jest.fn().mockReturnValue({
16
+ errorBeacon: 'example.com',
17
+ licenseKey: 'abcd'
18
+ }),
19
+ getRuntime: jest.fn().mockReturnValue({
20
+ bytesSent: {},
21
+ queryBytesSent: {}
22
+ })
23
+ }))
24
+ jest.mock('../util/submit-data', () => ({
25
+ __esModule: true,
26
+ submitData: {
27
+ xhr: jest.fn(() => ({
28
+ addEventListener: jest.fn()
29
+ })),
30
+ beacon: jest.fn(),
31
+ img: jest.fn()
32
+ }
33
+ }))
34
+
35
+ describe('sendX', () => {
36
+ test.each([null, undefined, false])('should not send request when body is empty and sendEmptyBody is %s', (sendEmptyBody) => {
37
+ const sendCallback = jest.fn()
38
+ const harvester = new Harvest()
39
+ harvester.on('jserrors', () => ({
40
+ body: {},
41
+ qs: {}
42
+ }))
43
+
44
+ harvester.sendX({ endpoint: 'jserrors', cbFinished: sendCallback })
45
+
46
+ expect(sendCallback).toHaveBeenCalledWith({ sent: false })
47
+ expect(submitData.xhr).not.toHaveBeenCalled()
48
+ expect(submitData.img).not.toHaveBeenCalled()
49
+ expect(submitData.beacon).not.toHaveBeenCalled()
50
+ })
51
+
52
+ test('should send request when body is empty and sendEmptyBody is true', () => {
53
+ const harvester = new Harvest()
54
+ harvester.on('jserrors', () => ({
55
+ body: {},
56
+ qs: {}
57
+ }))
58
+
59
+ harvester.sendX({ endpoint: 'jserrors', opts: { sendEmptyBody: true }, cbFinished: jest.fn() })
60
+
61
+ expect(submitData.xhr).toHaveBeenCalledWith(expect.objectContaining({
62
+ url: expect.stringContaining('https://example.com/jserrors/1/abcd?'),
63
+ body: undefined
64
+ }))
65
+ expect(submitData.img).not.toHaveBeenCalled()
66
+ expect(submitData.beacon).not.toHaveBeenCalled()
67
+ })
68
+
69
+ test.each([null, undefined, []])('should remove %s values from the body and query string when sending', (emptyValue) => {
70
+ const harvester = new Harvest()
71
+ harvester.on('jserrors', () => ({
72
+ body: { bar: 'foo', empty: emptyValue },
73
+ qs: { foo: 'bar', empty: emptyValue }
74
+ }))
75
+
76
+ harvester.sendX({ endpoint: 'jserrors', cbFinished: jest.fn() })
77
+
78
+ expect(submitData.xhr).toHaveBeenCalledWith(expect.objectContaining({
79
+ url: expect.stringContaining('&foo=bar'),
80
+ body: JSON.stringify({ bar: 'foo' })
81
+ }))
82
+ expect(submitData.xhr).toHaveBeenCalledWith(expect.objectContaining({
83
+ url: expect.not.stringContaining('&empty'),
84
+ body: expect.not.stringContaining('empty')
85
+ }))
86
+ expect(submitData.img).not.toHaveBeenCalled()
87
+ expect(submitData.beacon).not.toHaveBeenCalled()
88
+ })
89
+
90
+ test.each([1, false, true])('should not remove value %s (when it doesn\'t have a length) from the body and query string when sending', (nonStringValue) => {
91
+ const harvester = new Harvest()
92
+ harvester.on('jserrors', () => ({
93
+ body: { bar: 'foo', nonString: nonStringValue },
94
+ qs: { foo: 'bar', nonString: nonStringValue }
95
+ }))
96
+
97
+ harvester.sendX({ endpoint: 'jserrors', cbFinished: jest.fn() })
98
+
99
+ expect(submitData.xhr).toHaveBeenCalledWith(expect.objectContaining({
100
+ url: expect.stringContaining(`&nonString=${nonStringValue}`),
101
+ body: JSON.stringify({ bar: 'foo', nonString: nonStringValue })
102
+ }))
103
+ expect(submitData.img).not.toHaveBeenCalled()
104
+ expect(submitData.beacon).not.toHaveBeenCalled()
105
+ })
106
+ })
107
+
108
+ describe('send', () => {
109
+ test.each([null, undefined, false])('should not send request when body is empty and sendEmptyBody is %s', (sendEmptyBody) => {
110
+ const sendCallback = jest.fn()
111
+ const harvester = new Harvest()
112
+
113
+ harvester.send({ endpoint: 'rum', payload: { qs: {}, body: {} }, opts: { sendEmptyBody }, cbFinished: sendCallback })
114
+
115
+ expect(sendCallback).toHaveBeenCalledWith({ sent: false })
116
+ expect(submitData.xhr).not.toHaveBeenCalled()
117
+ expect(submitData.img).not.toHaveBeenCalled()
118
+ expect(submitData.beacon).not.toHaveBeenCalled()
119
+ })
120
+
121
+ test('should send request when body is empty and sendEmptyBody is true', () => {
122
+ const harvester = new Harvest()
123
+
124
+ harvester.send({ endpoint: 'rum', payload: { qs: {}, body: {} }, opts: { sendEmptyBody: true } })
125
+
126
+ expect(submitData.xhr).toHaveBeenCalledWith(expect.objectContaining({
127
+ url: expect.stringContaining('https://example.com/1/abcd?'),
128
+ body: undefined
129
+ }))
130
+ expect(submitData.img).not.toHaveBeenCalled()
131
+ expect(submitData.beacon).not.toHaveBeenCalled()
132
+ })
133
+
134
+ test.each([null, undefined, []])('should remove %s values from the body and query string when sending', (emptyValue) => {
135
+ const harvester = new Harvest()
136
+
137
+ harvester.send({
138
+ endpoint: 'rum',
139
+ payload: { qs: { foo: 'bar', empty: emptyValue }, body: { bar: 'foo', empty: emptyValue } }
140
+ })
141
+
142
+ expect(submitData.xhr).toHaveBeenCalledWith(expect.objectContaining({
143
+ url: expect.stringContaining('&foo=bar'),
144
+ body: JSON.stringify({ bar: 'foo' })
145
+ }))
146
+ expect(submitData.xhr).toHaveBeenCalledWith(expect.objectContaining({
147
+ url: expect.not.stringContaining('&empty'),
148
+ body: expect.not.stringContaining('empty')
149
+ }))
150
+ expect(submitData.img).not.toHaveBeenCalled()
151
+ expect(submitData.beacon).not.toHaveBeenCalled()
152
+ })
153
+
154
+ test.each([1, false, true])('should not remove value %s (when it doesn\'t have a length) from the body and query string when sending', (nonStringValue) => {
155
+ const harvester = new Harvest()
156
+
157
+ harvester.send({
158
+ endpoint: 'rum',
159
+ payload: { qs: { foo: 'bar', nonString: nonStringValue }, body: { bar: 'foo', nonString: nonStringValue } }
160
+ })
161
+
162
+ expect(submitData.xhr).toHaveBeenCalledWith(expect.objectContaining({
163
+ url: expect.stringContaining(`&nonString=${nonStringValue}`),
164
+ body: JSON.stringify({ bar: 'foo', nonString: nonStringValue })
165
+ }))
166
+ expect(submitData.img).not.toHaveBeenCalled()
167
+ expect(submitData.beacon).not.toHaveBeenCalled()
168
+ })
169
+ })
@@ -1,4 +1,3 @@
1
-
2
1
  import { LocalMemory } from '../storage/local-memory'
3
2
  import { LocalStorage } from '../storage/local-storage'
4
3
 
@@ -1,4 +1,3 @@
1
-
2
1
  export class Timer {
3
2
  constructor (opts, ms) {
4
3
  if (!opts.onEnd) throw new Error('onEnd handler is required')
@@ -48,11 +48,11 @@ export function obj (payload, maxBytes) {
48
48
  var next
49
49
  var i
50
50
 
51
- if (typeof dataArray === 'string') {
51
+ if (typeof dataArray === 'string' || (!Array.isArray(dataArray) && dataArray !== null && dataArray !== undefined && dataArray.toString().length)) {
52
52
  next = '&' + feature + '=' + qs(dataArray)
53
53
  total += next.length
54
54
  result += next
55
- } else if (dataArray.length) {
55
+ } else if (Array.isArray(dataArray) && dataArray.length) {
56
56
  total += 9
57
57
  for (i = 0; i < dataArray.length; i++) {
58
58
  next = qs(stringify(dataArray[i]))
@@ -0,0 +1,34 @@
1
+ import { warn } from './console'
2
+
3
+ beforeEach(() => {
4
+ console.warn = jest.fn()
5
+ })
6
+
7
+ afterEach(() => {
8
+ jest.restoreAllMocks()
9
+ })
10
+
11
+ describe('warn', () => {
12
+ test('should not call console.warn if it is not a function', () => {
13
+ const spy = jest.spyOn(console, 'warn')
14
+ console.warn = undefined
15
+ warn('test message')
16
+ expect(spy).not.toHaveBeenCalled()
17
+ })
18
+
19
+ test('should call console.warn with a prefixed message', () => {
20
+ warn('test message')
21
+ expect(console.warn).toHaveBeenCalledWith('New Relic: test message')
22
+ })
23
+
24
+ test('should call console.warn with secondary argument if provided', () => {
25
+ const secondary = 'secondary value'
26
+ warn('test message', secondary)
27
+ expect(console.warn).toHaveBeenCalledWith(secondary)
28
+ })
29
+
30
+ test('should not call console.warn with secondary argument if not provided', () => {
31
+ warn('test message')
32
+ expect(console.warn).toHaveBeenCalledTimes(1)
33
+ })
34
+ })
@@ -5,7 +5,14 @@
5
5
 
6
6
  var has = Object.prototype.hasOwnProperty
7
7
 
8
- // Always returns the current value of obj[prop], even if it has to set it first
8
+ /**
9
+ * Always returns the current value of obj[prop], even if it has to set it first. Sets properties as non-enumerable if possible.
10
+ *
11
+ * @param {Object} obj - The object to get or set the property on.
12
+ * @param {string} prop - The name of the property.
13
+ * @param {Function} getVal - A function that returns the value to be set if the property does not exist.
14
+ * @returns {*} The value of the property in the object.
15
+ */
9
16
  export function getOrSet (obj, prop, getVal) {
10
17
  // If the value exists return it.
11
18
  if (has.call(obj, prop)) return obj[prop]
@@ -0,0 +1,58 @@
1
+ import { getOrSet } from './get-or-set'
2
+
3
+ test('should return the current value of an existing property', () => {
4
+ const obj = { foo: 'bar' }
5
+ const prop = 'foo'
6
+ const getVal = jest.fn()
7
+
8
+ const result = getOrSet(obj, prop, getVal)
9
+
10
+ expect(result).toBe('bar')
11
+ expect(getVal).not.toHaveBeenCalled()
12
+ })
13
+
14
+ test('should set and return the value from getVal if the property does not exist', () => {
15
+ const obj = {}
16
+ const prop = 'foo'
17
+ const getVal = jest.fn().mockReturnValue('baz')
18
+
19
+ const result = getOrSet(obj, prop, getVal)
20
+
21
+ expect(result).toBe('baz')
22
+ expect(getVal).toHaveBeenCalled()
23
+ expect(obj.foo).toBe('baz')
24
+ })
25
+
26
+ test('should set the property as non-enumerable if Object.defineProperty is supported', () => {
27
+ const obj = {}
28
+ const prop = 'foo'
29
+ const getVal = jest.fn().mockReturnValue('baz')
30
+
31
+ jest.spyOn(Object, 'defineProperty')
32
+
33
+ const result = getOrSet(obj, prop, getVal)
34
+
35
+ expect(result).toBe('baz')
36
+ expect(Object.defineProperty).toHaveBeenCalledWith(obj, prop, {
37
+ value: 'baz',
38
+ writable: true,
39
+ enumerable: false
40
+ })
41
+ })
42
+
43
+ test('should set the property from getVal if Object.defineProperty and Object.keys are not supported', () => {
44
+ const obj = {}
45
+ const prop = 'foo'
46
+ const getVal = jest.fn(() => 'baz')
47
+
48
+ const originalFn = Object.defineProperty
49
+ Object.defineProperty = null
50
+
51
+ const result = getOrSet(obj, prop, getVal)
52
+
53
+ expect(result).toBe('baz')
54
+ expect(obj.foo).toBe('baz')
55
+ expect(Object.prototype.propertyIsEnumerable.call(obj, 'foo')).toBe(true)
56
+
57
+ Object.defineProperty = originalFn
58
+ })
@@ -0,0 +1,49 @@
1
+ import { stringify } from './stringify'
2
+
3
+ var mockEmit = jest.fn()
4
+ jest.mock('../event-emitter/contextual-ee', () => ({
5
+ __esModule: true,
6
+ get ee () {
7
+ return { emit: mockEmit }
8
+ }
9
+ }))
10
+
11
+ test('should return a JSON string representation of the value', () => {
12
+ const obj = { a: 1, b: { nested: true } }
13
+ const expected = '{"a":1,"b":{"nested":true}}'
14
+
15
+ const result = stringify(obj)
16
+
17
+ expect(result).toBe(expected)
18
+ })
19
+
20
+ test('should handle circular references and exclude them from the JSON output', () => {
21
+ const obj = { a: 1 }
22
+ obj.b = obj // Create a circular reference
23
+ const expected = '{"a":1}'
24
+
25
+ const result = stringify(obj)
26
+
27
+ expect(result).toBe(expected)
28
+ })
29
+
30
+ test('should handle non-object values and return their string representation', () => {
31
+ const value = 42
32
+ const expected = '42'
33
+
34
+ const result = stringify(value)
35
+
36
+ expect(result).toBe(expected)
37
+ })
38
+
39
+ test('should emit an "internal-error" event if an error occurs during JSON.stringify', () => {
40
+ const obj = { a: 1 }
41
+
42
+ jest.spyOn(JSON, 'stringify').mockImplementation(() => {
43
+ throw new Error('message')
44
+ })
45
+
46
+ stringify(obj)
47
+
48
+ expect(mockEmit).toHaveBeenCalledWith('internal-error', expect.any(Array))
49
+ })
@@ -33,25 +33,32 @@ submitData.jsonp = function jsonp ({ url, jsonp }) {
33
33
  return element
34
34
  }
35
35
  } catch (err) {
36
- // do nothing
36
+ // do nothing
37
37
  }
38
38
  }
39
39
 
40
+ /**
41
+ * Performs an asynchronous GET request using XMLHttpRequest.
42
+ *
43
+ * @param {Object} args - An object containing a `url` property.
44
+ * @param {string} args.url - The URL to send the GET request to.
45
+ * @returns {XMLHttpRequest} - An XMLHttpRequest object.
46
+ */
40
47
  submitData.xhrGet = function xhrGet ({ url }) {
41
48
  return submitData.xhr({ url, sync: false, method: 'GET' })
42
49
  }
43
50
 
44
51
  /**
45
52
  * Send via XHR
46
- * @param {Object} args - The args
47
- * @param {string} args.url - The URL to send to
48
- * @param {string=} args.body - The Stringified body
49
- * @param {boolean=} args.sync - Run XHR as Synchronous
50
- * @param {string=} [args.method=POST] - The XHR method to use
51
- * @param {{key: string, value: string}[]} [args.headers] - The headers to attach
53
+ * @param {Object} args - The args.
54
+ * @param {string} args.url - The URL to send to.
55
+ * @param {string=} args.body - The Stringified body. Default null to prevent IE11 from breaking.
56
+ * @param {boolean=} args.sync - Run XHR synchronously.
57
+ * @param {string=} [args.method=POST] - The XHR method to use.
58
+ * @param {{key: string, value: string}[]} [args.headers] - The headers to attach.
52
59
  * @returns {XMLHttpRequest}
53
60
  */
54
- submitData.xhr = function xhr ({ url, body, sync, method = 'POST', headers = [{ key: 'content-type', value: 'text/plain' }] }) {
61
+ submitData.xhr = function xhr ({ url, body = null, sync, method = 'POST', headers = [{ key: 'content-type', value: 'text/plain' }] }) {
55
62
  var request = new XMLHttpRequest()
56
63
 
57
64
  request.open(method, url, !sync)
@@ -70,13 +77,6 @@ submitData.xhr = function xhr ({ url, body, sync, method = 'POST', headers = [{
70
77
  return request
71
78
  }
72
79
 
73
- /**
74
- * Unused at the moment -- DEPRECATED
75
- */
76
- // submitData.xhrSync = function xhrSync (url, body) {
77
- // return submitData.xhr(url, body, true)
78
- // }
79
-
80
80
  /**
81
81
  * Send by appending an <img> element to the page. Do NOT call this function outside of a guaranteed web window environment.
82
82
  * @param {Object} args - The args
@@ -84,7 +84,6 @@ submitData.xhr = function xhr ({ url, body, sync, method = 'POST', headers = [{
84
84
  * @returns {HTMLImageElement}
85
85
  */
86
86
  submitData.img = function img ({ url }) {
87
- console.log('img url', url)
88
87
  var element = new Image()
89
88
  element.src = url
90
89
  return element