@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.
- package/dist/cjs/cdn/experimental.js +27 -0
- package/dist/cjs/common/config/state/init.js +1 -1
- package/dist/cjs/common/constants/env.cdn.js +1 -1
- package/dist/cjs/common/constants/env.npm.js +1 -1
- package/dist/cjs/common/event-emitter/contextual-ee.test.js +10 -10
- package/dist/cjs/common/harvest/harvest-scheduler.js +18 -3
- package/dist/cjs/common/harvest/harvest-scheduler.test.js +39 -0
- package/dist/cjs/common/harvest/harvest.js +14 -34
- package/dist/cjs/common/harvest/harvest.test.js +224 -0
- package/dist/cjs/common/url/encode.js +2 -2
- package/dist/cjs/common/util/console.test.js +30 -0
- package/dist/cjs/common/util/get-or-set.js +8 -1
- package/dist/cjs/common/util/get-or-set.test.js +47 -0
- package/dist/cjs/common/util/stringify.test.js +48 -0
- package/dist/cjs/common/util/submit-data.js +15 -15
- package/dist/cjs/common/util/submit-data.test.js +221 -0
- package/dist/cjs/common/util/traverse.js +19 -27
- package/dist/cjs/common/util/traverse.test.js +44 -0
- package/dist/cjs/features/metrics/aggregate/endpoint-map.js +14 -0
- package/dist/cjs/features/metrics/aggregate/index.js +3 -2
- package/dist/cjs/features/metrics/instrument/index.js +0 -2
- package/dist/cjs/features/page_view_event/aggregate/index.js +58 -44
- package/dist/cjs/features/session_replay/aggregate/index.js +10 -7
- package/dist/cjs/loaders/configure/configure.js +0 -1
- package/dist/esm/cdn/experimental.js +24 -0
- package/dist/esm/common/config/state/init.js +1 -1
- package/dist/esm/common/constants/env.cdn.js +1 -1
- package/dist/esm/common/constants/env.npm.js +1 -1
- package/dist/esm/common/event-emitter/contextual-ee.test.js +10 -10
- package/dist/esm/common/harvest/harvest-scheduler.js +18 -3
- package/dist/esm/common/harvest/harvest-scheduler.test.js +37 -0
- package/dist/esm/common/harvest/harvest.js +14 -34
- package/dist/esm/common/harvest/harvest.test.js +222 -0
- package/dist/esm/common/url/encode.js +2 -2
- package/dist/esm/common/util/console.test.js +28 -0
- package/dist/esm/common/util/get-or-set.js +8 -1
- package/dist/esm/common/util/get-or-set.test.js +45 -0
- package/dist/esm/common/util/stringify.test.js +46 -0
- package/dist/esm/common/util/submit-data.js +15 -15
- package/dist/esm/common/util/submit-data.test.js +219 -0
- package/dist/esm/common/util/traverse.js +19 -27
- package/dist/esm/common/util/traverse.test.js +42 -0
- package/dist/esm/features/metrics/aggregate/endpoint-map.js +7 -0
- package/dist/esm/features/metrics/aggregate/index.js +3 -2
- package/dist/esm/features/metrics/instrument/index.js +0 -2
- package/dist/esm/features/page_view_event/aggregate/index.js +58 -44
- package/dist/esm/features/session_replay/aggregate/index.js +10 -7
- package/dist/esm/loaders/configure/configure.js +0 -1
- package/dist/types/cdn/experimental.d.ts +2 -0
- package/dist/types/cdn/experimental.d.ts.map +1 -0
- package/dist/types/common/config/state/init.d.ts.map +1 -1
- package/dist/types/common/context/shared-context.d.ts.map +1 -1
- package/dist/types/common/harvest/harvest-scheduler.d.ts +26 -3
- package/dist/types/common/harvest/harvest-scheduler.d.ts.map +1 -1
- package/dist/types/common/harvest/harvest.d.ts +2 -2
- package/dist/types/common/harvest/harvest.d.ts.map +1 -1
- package/dist/types/common/timer/timer.d.ts.map +1 -1
- package/dist/types/common/util/get-or-set.d.ts +9 -1
- package/dist/types/common/util/get-or-set.d.ts.map +1 -1
- package/dist/types/common/util/submit-data.d.ts +14 -10
- package/dist/types/common/util/submit-data.d.ts.map +1 -1
- package/dist/types/common/util/traverse.d.ts +10 -1
- package/dist/types/common/util/traverse.d.ts.map +1 -1
- package/dist/types/common/window/nreum.d.ts.map +1 -1
- package/dist/types/features/metrics/aggregate/endpoint-map.d.ts +8 -0
- package/dist/types/features/metrics/aggregate/endpoint-map.d.ts.map +1 -0
- package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/metrics/aggregate/polyfill-detection.es5.d.ts.map +1 -1
- package/dist/types/features/metrics/instrument/index.d.ts.map +1 -1
- package/dist/types/features/page_view_event/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/page_view_event/instrument/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/aggregate/index.d.ts +4 -0
- package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/utils/handler-cache.d.ts.map +1 -1
- package/dist/types/loaders/configure/configure.d.ts.map +1 -1
- package/package.json +8 -3
- package/src/cdn/experimental.js +36 -0
- package/src/common/config/state/init.js +1 -2
- package/src/common/context/shared-context.js +0 -1
- package/src/common/event-emitter/contextual-ee.test.js +10 -10
- package/src/common/harvest/harvest-scheduler.js +18 -3
- package/src/common/harvest/harvest-scheduler.test.js +25 -0
- package/src/common/harvest/harvest.js +15 -20
- package/src/common/harvest/harvest.test.js +169 -0
- package/src/common/session/session-entity.test.js +0 -1
- package/src/common/timer/timer.js +0 -1
- package/src/common/url/encode.js +2 -2
- package/src/common/util/console.test.js +34 -0
- package/src/common/util/get-or-set.js +8 -1
- package/src/common/util/get-or-set.test.js +58 -0
- package/src/common/util/stringify.test.js +49 -0
- package/src/common/util/submit-data.js +15 -16
- package/src/common/util/submit-data.test.js +218 -0
- package/src/common/util/traverse.js +18 -27
- package/src/common/util/traverse.test.js +50 -0
- package/src/common/window/nreum.js +0 -1
- package/src/features/metrics/aggregate/endpoint-map.js +7 -0
- package/src/features/metrics/aggregate/index.js +3 -2
- package/src/features/metrics/aggregate/polyfill-detection.es5.js +0 -1
- package/src/features/metrics/instrument/index.js +0 -2
- package/src/features/page_view_event/aggregate/index.js +48 -51
- package/src/features/page_view_event/instrument/index.js +0 -1
- package/src/features/session_replay/aggregate/index.js +12 -7
- package/src/features/utils/handler-cache.js +0 -1
- package/src/loaders/configure/configure.js +0 -1
- package/dist/cjs/common/util/s-hash.js +0 -19
- package/dist/cjs/features/metrics/instrument/workers-helper.js +0 -124
- package/dist/esm/common/util/s-hash.js +0 -13
- package/dist/esm/features/metrics/instrument/workers-helper.js +0 -119
- package/dist/types/common/util/s-hash.d.ts +0 -2
- package/dist/types/common/util/s-hash.d.ts.map +0 -1
- package/dist/types/features/metrics/instrument/workers-helper.d.ts +0 -7
- package/dist/types/features/metrics/instrument/workers-helper.d.ts.map +0 -1
- package/src/common/util/s-hash.js +0 -14
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
+
})
|
package/src/common/url/encode.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|