@newrelic/browser-agent 1.303.0-rc.8 → 1.303.0-rc.9

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 (51) hide show
  1. package/dist/cjs/common/constants/env.cdn.js +1 -1
  2. package/dist/cjs/common/constants/env.npm.js +1 -1
  3. package/dist/cjs/common/window/page-visibility.js +4 -0
  4. package/dist/cjs/common/wrap/wrap-websocket.js +262 -32
  5. package/dist/cjs/features/generic_events/aggregate/index.js +24 -8
  6. package/dist/cjs/features/generic_events/instrument/index.js +13 -3
  7. package/dist/cjs/features/jserrors/aggregate/index.js +4 -1
  8. package/dist/cjs/features/metrics/aggregate/index.js +0 -6
  9. package/dist/cjs/features/metrics/constants.js +2 -4
  10. package/dist/cjs/features/metrics/instrument/index.js +0 -11
  11. package/dist/cjs/features/page_view_timing/instrument/index.js +1 -2
  12. package/dist/esm/common/constants/env.cdn.js +1 -1
  13. package/dist/esm/common/constants/env.npm.js +1 -1
  14. package/dist/esm/common/window/page-visibility.js +4 -1
  15. package/dist/esm/common/wrap/wrap-websocket.js +262 -31
  16. package/dist/esm/features/generic_events/aggregate/index.js +24 -8
  17. package/dist/esm/features/generic_events/instrument/index.js +13 -3
  18. package/dist/esm/features/jserrors/aggregate/index.js +4 -1
  19. package/dist/esm/features/metrics/aggregate/index.js +0 -6
  20. package/dist/esm/features/metrics/constants.js +1 -4
  21. package/dist/esm/features/metrics/instrument/index.js +1 -14
  22. package/dist/esm/features/page_view_timing/instrument/index.js +2 -3
  23. package/dist/tsconfig.tsbuildinfo +1 -1
  24. package/dist/types/common/window/page-visibility.d.ts +1 -0
  25. package/dist/types/common/window/page-visibility.d.ts.map +1 -1
  26. package/dist/types/common/wrap/wrap-websocket.d.ts +0 -2
  27. package/dist/types/common/wrap/wrap-websocket.d.ts.map +1 -1
  28. package/dist/types/features/generic_events/aggregate/index.d.ts +0 -1
  29. package/dist/types/features/generic_events/aggregate/index.d.ts.map +1 -1
  30. package/dist/types/features/generic_events/instrument/index.d.ts.map +1 -1
  31. package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
  32. package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
  33. package/dist/types/features/metrics/constants.d.ts +0 -1
  34. package/dist/types/features/metrics/constants.d.ts.map +1 -1
  35. package/dist/types/features/metrics/instrument/index.d.ts.map +1 -1
  36. package/dist/types/features/page_view_timing/instrument/index.d.ts.map +1 -1
  37. package/package.json +1 -1
  38. package/src/common/window/page-visibility.js +5 -1
  39. package/src/common/wrap/wrap-websocket.js +248 -30
  40. package/src/features/generic_events/aggregate/index.js +26 -8
  41. package/src/features/generic_events/instrument/index.js +13 -3
  42. package/src/features/jserrors/aggregate/index.js +4 -1
  43. package/src/features/metrics/aggregate/index.js +0 -6
  44. package/src/features/metrics/constants.js +0 -4
  45. package/src/features/metrics/instrument/index.js +0 -10
  46. package/src/features/page_view_timing/instrument/index.js +2 -3
  47. package/dist/cjs/features/metrics/aggregate/websocket-detection.js +0 -39
  48. package/dist/esm/features/metrics/aggregate/websocket-detection.js +0 -33
  49. package/dist/types/features/metrics/aggregate/websocket-detection.d.ts +0 -12
  50. package/dist/types/features/metrics/aggregate/websocket-detection.d.ts.map +0 -1
  51. package/src/features/metrics/aggregate/websocket-detection.js +0 -35
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/generic_events/aggregate/index.js"],"names":[],"mappings":"AAkBA;IACE,2BAAiC;IAGjC,2BAgOC;IA9NC,gCAAkG;IAiOpG;;;;;;;;;;;;OAYG;IACH,eAJW,MAAM,YAAC,WACP,MAAM,YAAC,QAmCjB;IAED,qCAEC;IAED;;;MAEC;IAED,gCAEC;;CAkBF;8BA5T6B,4BAA4B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/generic_events/aggregate/index.js"],"names":[],"mappings":"AAkBA;IACE,2BAAiC;IAGjC,2BAkPC;IAhPC,gCAAkG;IAmPpG;;;;;;;;;;;;OAYG;IACH,eAJW,MAAM,YAAC,WACP,MAAM,YAAC,QAmCjB;IAED,qCAEC;IAED;;;MAEC;;CAsBF;8BA9U6B,4BAA4B"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/generic_events/instrument/index.js"],"names":[],"mappings":"AAuBA;IACE,2BAAiC;IACjC,2BAgGC;IAXG,2CAA0C;IAG5C,yBAGC;CAMJ;AAED,8CAAuC;+BA9GR,6BAA6B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/generic_events/instrument/index.js"],"names":[],"mappings":"AAwBA;IACE,2BAAiC;IACjC,2BAyGC;IAXG,2CAA0C;IAG5C,yBAGC;CAMJ;AAED,8CAAuC;+BAxHR,6BAA6B"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/jserrors/aggregate/index.js"],"names":[],"mappings":"AAwBA;;GAEG;AAEH;IACE,2BAAiC;IACjC,2BA4BC;IAvBC,kBAAuB;IACvB,eAAoB;IACpB,qBAA0B;IAC1B,2BAAgC;IAChC,qBAAwB;IAqB1B,oDAEC;IAED;;;MAcC;IAED;;;;;;OAMG;IACH,qCAHW,SAAS,GACP,MAAM,CAgBlB;IAED;;;;;;;;;;OAUG;IACH,gBATW,KAAK,GAAC,aAAa,QACnB,MAAM,aACN,OAAO,YAAC,qBACR,MAAM,YAAC,cACP,OAAO,YAAC,kBACR,MAAM,YAAC,WACP,MAAM,YAAC,QA8GjB;IA+BD;;;;;MAKE;IACF,sCAHU,MAAM,GACJ,OAAO,CAIlB;IAGD,yDA6BC;IAED,8GAWC;;CACF;wBAzRY,OAAO,0BAA0B,EAAE,SAAS;8BAR3B,4BAA4B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/jserrors/aggregate/index.js"],"names":[],"mappings":"AAwBA;;GAEG;AAEH;IACE,2BAAiC;IACjC,2BA4BC;IAvBC,kBAAuB;IACvB,eAAoB;IACpB,qBAA0B;IAC1B,2BAAgC;IAChC,qBAAwB;IAqB1B,oDAEC;IAED;;;MAcC;IAED;;;;;;OAMG;IACH,qCAHW,SAAS,GACP,MAAM,CAgBlB;IAED;;;;;;;;;;OAUG;IACH,gBATW,KAAK,GAAC,aAAa,QACnB,MAAM,aACN,OAAO,YAAC,qBACR,MAAM,YAAC,cACP,OAAO,YAAC,kBACR,MAAM,YAAC,WACP,MAAM,YAAC,QAiHjB;IA+BD;;;;;MAKE;IACF,sCAHU,MAAM,GACJ,OAAO,CAIlB;IAGD,yDA6BC;IAED,8GAWC;;CACF;wBA5RY,OAAO,0BAA0B,EAAE,SAAS;8BAR3B,4BAA4B"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/metrics/aggregate/index.js"],"names":[],"mappings":"AAiBA;IACE,2BAAiC;IACjC,2BAgCC;IA5BC,uKAAuK;IACvK,oBAAyB;IAWzB,uCAAiE;IAkBnE,iCAAsE;IAEtE,wDAKC;IAED,iDAKC;IAED,qBAgFC;IAED,0BAOC;CACF;8BApJ6B,4BAA4B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/metrics/aggregate/index.js"],"names":[],"mappings":"AAiBA;IACE,2BAAiC;IACjC,2BAgCC;IA5BC,uKAAuK;IACvK,oBAAyB;IAWzB,uCAAiE;IAkBnE,iCAAsE;IAEtE,wDAKC;IAED,iDAKC;IAED,qBA0EC;IAED,0BAOC;CACF;8BA9I6B,4BAA4B"}
@@ -3,5 +3,4 @@ export const SUPPORTABILITY_METRIC: "sm";
3
3
  export const CUSTOM_METRIC: "cm";
4
4
  export const SUPPORTABILITY_METRIC_CHANNEL: "storeSupportabilityMetrics";
5
5
  export const CUSTOM_METRIC_CHANNEL: "storeEventMetrics";
6
- export const WATCHABLE_WEB_SOCKET_EVENTS: string[];
7
6
  //# sourceMappingURL=constants.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../../src/features/metrics/constants.js"],"names":[],"mappings":"AAQA,kCAAiD;AACjD,oCAAqC,IAAI,CAAA;AACzC,4BAA6B,IAAI,CAAA;AACjC,4CAA6C,4BAA4B,CAAA;AACzE,oCAAqC,mBAAmB,CAAA;AAExD,mDAA2F"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../../src/features/metrics/constants.js"],"names":[],"mappings":"AAMA,kCAAiD;AACjD,oCAAqC,IAAI,CAAA;AACzC,4BAA6B,IAAI,CAAA;AACjC,4CAA6C,4BAA4B,CAAA;AACzE,oCAAqC,mBAAmB,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/metrics/instrument/index.js"],"names":[],"mappings":"AAgBA;IACE,2BAAiC;IACjC,2BAiBC;CACF;AAED,wCAAiC;+BA/BF,6BAA6B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/metrics/instrument/index.js"],"names":[],"mappings":"AAaA;IACE,2BAAiC;IACjC,2BAUC;CACF;AAED,wCAAiC;+BArBF,6BAA6B"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/page_view_timing/instrument/index.js"],"names":[],"mappings":"AAYA;IACE,2BAAiC;IACjC,2BAWC;CACF;AAED,+CAAwC;+BArBT,6BAA6B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/page_view_timing/instrument/index.js"],"names":[],"mappings":"AAWA;IACE,2BAAiC;IACjC,2BAWC;CACF;AAED,+CAAwC;+BArBT,6BAA6B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newrelic/browser-agent",
3
- "version": "1.303.0-rc.8",
3
+ "version": "1.303.0-rc.9",
4
4
  "private": false,
5
5
  "author": "New Relic Browser Agent Team <browser-agent@newrelic.com>",
6
6
  "description": "New Relic Browser Agent",
@@ -3,7 +3,7 @@
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
5
 
6
- import { documentAddEventListener } from '../event-listener/event-listener-opts'
6
+ import { documentAddEventListener, windowAddEventListener } from '../event-listener/event-listener-opts'
7
7
 
8
8
  /**
9
9
  * @param {function} cb - called when a visibility change occurs with the vis state at that time
@@ -21,3 +21,7 @@ export function subscribeToVisibilityChange (cb, toHiddenOnly = false, capture,
21
21
  cb(document.visibilityState)
22
22
  }
23
23
  }
24
+
25
+ export function subscribeToPageUnload (cb, capture, abortSignal) {
26
+ windowAddEventListener('pagehide', cb, capture, abortSignal)
27
+ }
@@ -3,59 +3,277 @@
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
5
  import { globalScope } from '../constants/runtime'
6
- import { now } from '../timing/now'
7
- import { checkState } from '../window/load'
8
6
  import { generateRandomHexString } from '../ids/unique-id'
7
+ import { now } from '../timing/now'
9
8
  import { gosNREUMOriginals } from '../window/nreum'
10
-
11
- export const WEBSOCKET_TAG = 'websocket-'
12
- export const ADD_EVENT_LISTENER_TAG = 'addEventListener'
9
+ import { subscribeToPageUnload } from '../window/page-visibility'
13
10
 
14
11
  const wrapped = {}
12
+ const openWebSockets = new Set() // track all instances to close out metrics on page unload
15
13
 
16
14
  export function wrapWebSocket (sharedEE) {
17
- if (wrapped[sharedEE.debugId]++) return sharedEE
18
15
  const originals = gosNREUMOriginals().o
19
16
  if (!originals.WS) return sharedEE
20
17
 
21
- function reporter (socketId) {
22
- const createdAt = now()
23
- return function (message, ...data) {
24
- const timestamp = data[0]?.timeStamp || now()
25
- const isLoaded = checkState()
26
- sharedEE.emit(WEBSOCKET_TAG + message, [timestamp, timestamp - createdAt, isLoaded, socketId, ...data])
27
- }
28
- }
18
+ const wsEE = sharedEE.get('websockets')
19
+ if (wrapped[wsEE.debugId]++) return wsEE
20
+ wrapped[wsEE.debugId] = 1 // otherwise, first feature to wrap events
21
+
22
+ // This handles page navigation scenarios where the browser closes WebSockets after pagehide fires
23
+ subscribeToPageUnload(() => {
24
+ const unloadTime = now()
25
+ openWebSockets.forEach(ws => {
26
+ ws.nrData.closedAt = unloadTime
27
+ ws.nrData.closeCode = 1001 // Going Away - standard code for page navigation
28
+ ws.nrData.closeReason = 'Page navigating away'
29
+ ws.nrData.closeWasClean = false
30
+ if (ws.nrData.openedAt) {
31
+ ws.nrData.connectedDuration = unloadTime - ws.nrData.openedAt
32
+ }
33
+
34
+ wsEE.emit('ws', [ws.nrData], ws)
35
+ })
36
+ })
29
37
 
30
38
  class WrappedWebSocket extends WebSocket {
31
39
  static name = 'WebSocket'
40
+ static toString () { // fake native WebSocket when static class is stringified
41
+ return 'function WebSocket() { [native code] }'
42
+ }
43
+
44
+ toString () { // fake [object WebSocket] when instance is stringified
45
+ return '[object WebSocket]'
46
+ }
47
+
48
+ get [Symbol.toStringTag] () { // fake [object WebSocket] when Object.prototype.toString.call is used on instance
49
+ return WrappedWebSocket.name
50
+ }
51
+
52
+ // Private method to tag send, close, and event listener errors with WebSocket ID for JSErrors feature
53
+ #tagError (error) {
54
+ ;(error.__newrelic ??= {}).socketId = this.nrData.socketId
55
+ this.nrData.hasErrors ??= true
56
+ }
32
57
 
33
58
  constructor (...args) {
34
59
  super(...args)
35
- const socketId = generateRandomHexString(6)
36
- this.report = reporter(socketId)
37
- this.report('new')
38
-
39
- const events = ['message', 'error', 'open', 'close']
40
- /** add event listeners */
41
- events.forEach(evt => {
42
- this.addEventListener(evt, function (e) {
43
- this.report(ADD_EVENT_LISTENER_TAG, { eventType: evt, event: e })
60
+ /** @type {WebSocketData} */
61
+ this.nrData = new WebSocketData(args[0], args[1])
62
+
63
+ this.addEventListener('open', () => {
64
+ this.nrData.openedAt = now()
65
+ ;['protocol', 'extensions', 'binaryType'].forEach(prop => {
66
+ this.nrData[prop] = this[prop]
44
67
  })
68
+ openWebSockets.add(this)
69
+ })
70
+
71
+ this.addEventListener('message', (event) => {
72
+ const { type, size } = getDataInfo(event.data)
73
+ this.nrData.messageOrigin ??= event.origin // the origin of messages thru WS lifetime cannot be changed, so set once is sufficient
74
+ this.nrData.messageCount = (this.nrData.messageCount ?? 0) + 1
75
+ this.nrData.messageBytes = (this.nrData.messageBytes ?? 0) + size
76
+ this.nrData.messageBytesMin = Math.min(this.nrData.messageBytesMin ?? Infinity, size)
77
+ this.nrData.messageBytesMax = Math.max(this.nrData.messageBytesMax ?? 0, size)
78
+ if (!(this.nrData.messageTypes ?? '').includes(type)) {
79
+ this.nrData.messageTypes = this.nrData.messageTypes ? `${this.nrData.messageTypes},${type}` : type
80
+ }
45
81
  })
82
+
83
+ this.addEventListener('close', (event) => {
84
+ this.nrData.closedAt = now()
85
+ this.nrData.closeCode = event.code
86
+ this.nrData.closeReason = event.reason
87
+ this.nrData.closeWasClean = event.wasClean
88
+ this.nrData.connectedDuration = this.nrData.closedAt - this.nrData.openedAt
89
+
90
+ openWebSockets.delete(this) // remove from tracking set since it's now closed
91
+ wsEE.emit('ws', [this.nrData], this)
92
+ })
93
+ }
94
+
95
+ addEventListener (type, listener, ...rest) {
96
+ const wsInstance = this
97
+ const wrappedListener = typeof listener === 'function'
98
+ ? function (...args) {
99
+ try {
100
+ return listener.apply(this, args)
101
+ } catch (error) {
102
+ wsInstance.#tagError(error)
103
+ throw error
104
+ }
105
+ }
106
+ : listener?.handleEvent
107
+ ? { // case for listener === object with handleEvent
108
+ handleEvent: function (...args) {
109
+ try {
110
+ return listener.handleEvent.apply(listener, args)
111
+ } catch (error) {
112
+ wsInstance.#tagError(error)
113
+ throw error
114
+ }
115
+ }
116
+ }
117
+ : listener // case for listener === null
118
+ return super.addEventListener(type, wrappedListener, ...rest)
46
119
  }
47
120
 
48
- send (...args) {
49
- this.report('send', ...args)
121
+ send (data) {
122
+ // Only track metrics if the connection is OPEN; data sent in CONNECTING state throws, and data sent in CLOSING/CLOSED states is silently discarded
123
+ if (this.readyState === WebSocket.OPEN) {
124
+ const { type, size } = getDataInfo(data)
125
+ this.nrData.sendCount = (this.nrData.sendCount ?? 0) + 1
126
+ this.nrData.sendBytes = (this.nrData.sendBytes ?? 0) + size
127
+ this.nrData.sendBytesMin = Math.min(this.nrData.sendBytesMin ?? Infinity, size)
128
+ this.nrData.sendBytesMax = Math.max(this.nrData.sendBytesMax ?? 0, size)
129
+ if (!(this.nrData.sendTypes ?? '').includes(type)) {
130
+ this.nrData.sendTypes = this.nrData.sendTypes ? `${this.nrData.sendTypes},${type}` : type
131
+ }
132
+ }
50
133
  try {
51
- return super.send(...args)
52
- } catch (err) {
53
- this.report('send-err', ...args)
54
- throw err
134
+ return super.send(data)
135
+ } catch (error) {
136
+ this.#tagError(error)
137
+ throw error
138
+ }
139
+ }
140
+
141
+ close (...args) {
142
+ try {
143
+ super.close(...args)
144
+ } catch (error) {
145
+ this.#tagError(error)
146
+ throw error
55
147
  }
56
148
  }
57
149
  }
58
150
 
59
151
  globalScope.WebSocket = WrappedWebSocket
60
- return sharedEE
152
+ return wsEE
153
+ }
154
+
155
+ /**
156
+ * Returns the data type and size of the WebSocket send data
157
+ * @param {*} data - The data being sent
158
+ * @returns {{ type: string, size: number }} - The type name and size in bytes
159
+ */
160
+ function getDataInfo (data) {
161
+ if (typeof data === 'string') {
162
+ return {
163
+ type: 'string',
164
+ size: new TextEncoder().encode(data).length // efficient way to calculate the # of UTF-8 bytes that WS sends (cannot use string length)
165
+ }
166
+ }
167
+ if (data instanceof ArrayBuffer) {
168
+ return { type: 'ArrayBuffer', size: data.byteLength }
169
+ }
170
+ if (data instanceof Blob) {
171
+ return { type: 'Blob', size: data.size }
172
+ }
173
+ if (data instanceof DataView) {
174
+ return { type: 'DataView', size: data.byteLength }
175
+ }
176
+ if (ArrayBuffer.isView(data)) {
177
+ return { type: 'TypedArray', size: data.byteLength }
178
+ }
179
+ return { type: 'unknown', size: 0 }
180
+ }
181
+
182
+ /**
183
+ * WebSocket instrumentation data model
184
+ */
185
+ class WebSocketData {
186
+ /**
187
+ * @param {string} requestedUrl - The URL passed to WebSocket constructor
188
+ * @param {string|string[]} [requestedProtocols] - The protocols passed to WebSocket constructor
189
+ */
190
+ constructor (requestedUrl, requestedProtocols) {
191
+ /** @type {number} Timestamp when the WebSocket was constructed (relative time); will be time corrected later when timeKeeper is available */
192
+ this.timestamp = now()
193
+
194
+ /** @type {string} Most current URL when WebSocket was created; relevant for SPA */
195
+ this.currentUrl = window.location.href
196
+
197
+ /*
198
+ * pageUrl will be set by addEvent later; unlike timestamp and currentUrl, it's not sensitive to *when* it is set.
199
+ * It should not be explicitly defined here as it will overwrite the default provided by Generic Events later.
200
+ */
201
+
202
+ /** @type {string} Unique identifier for this WebSocket connection */
203
+ this.socketId = generateRandomHexString(8)
204
+
205
+ /** @type {string} The URL requested for the WebSocket connection */
206
+ this.requestedUrl = requestedUrl
207
+
208
+ /** @type {string} Comma-separated list of requested protocols */
209
+ this.requestedProtocols = Array.isArray(requestedProtocols) ? requestedProtocols.join(',') : (requestedProtocols || '')
210
+
211
+ // Properties set when connection opens
212
+ /** @type {number} [openedAt] Timestamp when connection opened */
213
+ this.openedAt = undefined
214
+
215
+ /** @type {string} [protocol] The sub-protocol selected by the server */
216
+ this.protocol = undefined
217
+
218
+ /** @type {string} [extensions] The extensions selected by the server */
219
+ this.extensions = undefined
220
+
221
+ /** @type {string} [binaryType] The binary type ('blob' or 'arraybuffer') */
222
+ this.binaryType = undefined
223
+
224
+ // Message received metrics
225
+ /** @type {string} [messageOrigin] Origin of messages (set once) */
226
+ this.messageOrigin = undefined
227
+
228
+ /** @type {number} [messageCount] Total number of messages received */
229
+ this.messageCount = undefined
230
+
231
+ /** @type {number} [messageBytes] Total bytes received */
232
+ this.messageBytes = undefined
233
+
234
+ /** @type {number} [messageBytesMin] Minimum message size received */
235
+ this.messageBytesMin = undefined
236
+
237
+ /** @type {number} [messageBytesMax] Maximum message size received */
238
+ this.messageBytesMax = undefined
239
+
240
+ /** @type {string} [messageTypes] Comma-separated list of message types received */
241
+ this.messageTypes = undefined
242
+
243
+ // Send metrics
244
+ /** @type {number} [sendCount] Total number of messages sent */
245
+ this.sendCount = undefined
246
+
247
+ /** @type {number} [sendBytes] Total bytes sent */
248
+ this.sendBytes = undefined
249
+
250
+ /** @type {number} [sendBytesMin] Minimum message size sent */
251
+ this.sendBytesMin = undefined
252
+
253
+ /** @type {number} [sendBytesMax] Maximum message size sent */
254
+ this.sendBytesMax = undefined
255
+
256
+ /** @type {string} [sendTypes] Comma-separated list of message types sent */
257
+ this.sendTypes = undefined
258
+
259
+ // Close metrics
260
+ /** @type {number} [closedAt] Timestamp when connection closed */
261
+ this.closedAt = undefined
262
+
263
+ /** @type {number} [closeCode] WebSocket close code */
264
+ this.closeCode = undefined
265
+
266
+ /** @type {string} [closeReason] WebSocket close reason */
267
+ this.closeReason = undefined
268
+
269
+ /** @type {boolean} [closeWasClean] Whether the connection closed cleanly */
270
+ this.closeWasClean = undefined
271
+
272
+ /** @type {number} [connectedDuration] Duration of the connection in milliseconds */
273
+ this.connectedDuration = undefined
274
+
275
+ // Error tracking
276
+ /** @type {boolean} [hasErrors] Whether any errors occurred */
277
+ this.hasErrors = undefined
278
+ }
61
279
  }
@@ -37,7 +37,7 @@ export class Aggregate extends AggregateBase {
37
37
  if (RESERVED_EVENT_TYPES.includes(eventType)) return warn(46)
38
38
  this.addEvent({
39
39
  eventType,
40
- timestamp: this.toEpoch(timestamp),
40
+ timestamp: this.#toEpoch(timestamp),
41
41
  ...attributes
42
42
  }, target)
43
43
  }, this.featureName, this.ee)
@@ -47,7 +47,7 @@ export class Aggregate extends AggregateBase {
47
47
  this.addEvent({
48
48
  ...attributes,
49
49
  eventType: 'PageAction',
50
- timestamp: this.toEpoch(timestamp),
50
+ timestamp: this.#toEpoch(timestamp),
51
51
  timeSinceLoad: timestamp / 1000,
52
52
  actionName: name,
53
53
  referrerUrl: this.referrerUrl,
@@ -72,7 +72,7 @@ export class Aggregate extends AggregateBase {
72
72
  const { target, timeStamp, type } = aggregatedUserAction.event
73
73
  const userActionEvent = {
74
74
  eventType: 'UserAction',
75
- timestamp: this.toEpoch(timeStamp),
75
+ timestamp: this.#toEpoch(timeStamp),
76
76
  action: type,
77
77
  actionCount: aggregatedUserAction.count,
78
78
  actionDuration: aggregatedUserAction.relativeMs[aggregatedUserAction.relativeMs.length - 1],
@@ -149,7 +149,7 @@ export class Aggregate extends AggregateBase {
149
149
  this.addEvent({
150
150
  ...detailObj,
151
151
  eventType: 'BrowserPerformance',
152
- timestamp: this.toEpoch(entry.startTime),
152
+ timestamp: this.#toEpoch(entry.startTime),
153
153
  entryName: entry.name,
154
154
  entryDuration: entry.duration,
155
155
  entryType: type
@@ -214,7 +214,7 @@ export class Aggregate extends AggregateBase {
214
214
  const event = {
215
215
  ...entryObject,
216
216
  eventType: 'BrowserPerformance',
217
- timestamp: Math.floor(agentRef.runtime.timeKeeper.correctRelativeTimestamp(entryObject.startTime)),
217
+ timestamp: this.#toEpoch(entryObject.startTime),
218
218
  entryName: cleanURL(name),
219
219
  entryDuration: duration,
220
220
  firstParty
@@ -233,7 +233,7 @@ export class Aggregate extends AggregateBase {
233
233
  const event = {
234
234
  ...customAttributes,
235
235
  eventType: 'BrowserPerformance',
236
- timestamp: Math.floor(agentRef.runtime.timeKeeper.correctRelativeTimestamp(start)),
236
+ timestamp: this.#toEpoch(start),
237
237
  entryName: n,
238
238
  entryDuration: duration,
239
239
  entryType: 'measure'
@@ -242,6 +242,24 @@ export class Aggregate extends AggregateBase {
242
242
  this.addEvent(event, target)
243
243
  }, this.featureName, this.ee)
244
244
 
245
+ if (agentRef.init.feature_flags.includes('websockets')) {
246
+ registerHandler('ws-complete', (nrData) => {
247
+ const event = {
248
+ ...nrData,
249
+ eventType: 'WebSocket',
250
+ timestamp: this.#toEpoch(nrData.timestamp),
251
+ openedAt: this.#toEpoch(nrData.openedAt),
252
+ closedAt: this.#toEpoch(nrData.closedAt)
253
+ }
254
+
255
+ // Report supportability metrics for WebSocket completion
256
+ this.reportSupportabilityMetric('WebSocket/Completed/Seen')
257
+ this.reportSupportabilityMetric('WebSocket/Completed/Bytes', stringify(event).length)
258
+
259
+ this.addEvent(event)
260
+ }, this.featureName, this.ee)
261
+ }
262
+
245
263
  this.drain()
246
264
  })
247
265
  }
@@ -274,7 +292,7 @@ export class Aggregate extends AggregateBase {
274
292
 
275
293
  const defaultEventAttributes = {
276
294
  /** should be overridden by the event-specific attributes, but just in case -- set it to now() */
277
- timestamp: Math.floor(this.agentRef.runtime.timeKeeper.correctRelativeTimestamp(now())),
295
+ timestamp: this.#toEpoch(now()),
278
296
  /** all generic events require pageUrl(s) */
279
297
  pageUrl: cleanURL('' + initialLocation),
280
298
  currentUrl: cleanURL('' + location),
@@ -302,7 +320,7 @@ export class Aggregate extends AggregateBase {
302
320
  return { ua: this.agentRef.info.userAttributes, at: this.agentRef.info.atts }
303
321
  }
304
322
 
305
- toEpoch (timestamp) {
323
+ #toEpoch (timestamp) {
306
324
  return Math.floor(this.agentRef.runtime.timeKeeper.correctRelativeTimestamp(timestamp))
307
325
  }
308
326
 
@@ -20,18 +20,23 @@ import { wrapFetch } from '../../../common/wrap/wrap-fetch'
20
20
  import { wrapXhr } from '../../../common/wrap/wrap-xhr'
21
21
  import { parseUrl } from '../../../common/url/parse-url'
22
22
  import { extractUrl } from '../../../common/url/extract-url'
23
+ import { wrapWebSocket } from '../../../common/wrap/wrap-websocket'
23
24
 
24
25
  export class Instrument extends InstrumentBase {
25
26
  static featureName = FEATURE_NAME
26
27
  constructor (agentRef) {
27
28
  super(agentRef, FEATURE_NAME)
29
+ const websocketsEnabled = agentRef.init.feature_flags.includes('websockets')
30
+ const ufEnabled = agentRef.init.feature_flags.includes('user_frustrations')
31
+
28
32
  /** config values that gate whether the generic events aggregator should be imported at all */
29
33
  const genericEventSourceConfigs = [
30
34
  agentRef.init.page_action.enabled,
31
35
  agentRef.init.performance.capture_marks,
32
36
  agentRef.init.performance.capture_measures,
37
+ agentRef.init.performance.resources.enabled,
33
38
  agentRef.init.user_actions.enabled,
34
- agentRef.init.performance.resources.enabled
39
+ websocketsEnabled
35
40
  ]
36
41
 
37
42
  /** feature specific APIs */
@@ -41,13 +46,13 @@ export class Instrument extends InstrumentBase {
41
46
  setupRegisterAPI(agentRef)
42
47
  setupMeasureAPI(agentRef)
43
48
 
44
- const ufEnabled = agentRef.init.feature_flags.includes('user_frustrations')
45
- let historyEE
49
+ let historyEE, websocketsEE
46
50
  if (isBrowserScope && ufEnabled) {
47
51
  wrapFetch(this.ee)
48
52
  wrapXhr(this.ee)
49
53
  historyEE = wrapHistory(this.ee)
50
54
  }
55
+ if (websocketsEnabled) websocketsEE = wrapWebSocket(this.ee)
51
56
 
52
57
  if (isBrowserScope) {
53
58
  if (agentRef.init.user_actions.enabled) {
@@ -106,6 +111,11 @@ export class Instrument extends InstrumentBase {
106
111
  observer.observe({ type: 'resource', buffered: true })
107
112
  }
108
113
  }
114
+ if (websocketsEnabled) { // this can apply outside browser scope such as in worker
115
+ websocketsEE.on('ws', (nrData) => {
116
+ handle('ws-complete', [nrData], undefined, this.featureName, this.ee)
117
+ })
118
+ }
109
119
 
110
120
  try {
111
121
  this.removeOnAbort = new AbortController()
@@ -192,10 +192,13 @@ export class Aggregate extends AggregateBase {
192
192
  // still send EE events for other features such as above, but stop this one from aggregating internal data
193
193
  if (this.blocked) return
194
194
 
195
- if (err?.__newrelic?.[this.agentIdentifier]) {
195
+ if (err.__newrelic?.[this.agentIdentifier]) {
196
196
  params._interactionId = err.__newrelic[this.agentIdentifier].interactionId
197
197
  params._interactionNodeId = err.__newrelic[this.agentIdentifier].interactionNodeId
198
198
  }
199
+ if (err.__newrelic?.socketId) {
200
+ customAttributes.socketId = err.__newrelic.socketId
201
+ }
199
202
 
200
203
  if (this.shouldAllowMainAgentToCapture(target)) {
201
204
  const softNavInUse = Boolean(this.agentRef.features?.[FEATURE_NAMES.softNav])
@@ -131,12 +131,6 @@ export class Aggregate extends AggregateBase {
131
131
  // webdriver detection
132
132
  if (navigator.webdriver) this.storeSupportabilityMetrics('Generic/WebDriver/Detected')
133
133
 
134
- // WATCHABLE_WEB_SOCKET_EVENTS.forEach(tag => {
135
- // registerHandler('buffered-' + WEBSOCKET_TAG + tag, (...args) => {
136
- // handleWebsocketEvents(this.storeSupportabilityMetrics.bind(this), tag, ...args)
137
- // }, this.featureName, this.ee)
138
- // })
139
-
140
134
  /** all the harvest metadata metrics need to be evaluated simulataneously at unload time so just temporarily buffer them and dont make SMs immediately from the data */
141
135
  registerHandler('harvest-metadata', (harvestMetadataObject = {}) => {
142
136
  try {
@@ -2,8 +2,6 @@
2
2
  * Copyright 2020-2025 New Relic, Inc. All rights reserved.
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
-
6
- import { ADD_EVENT_LISTENER_TAG } from '../../common/wrap/wrap-websocket'
7
5
  import { FEATURE_NAMES } from '../../loaders/features/features'
8
6
 
9
7
  export const FEATURE_NAME = FEATURE_NAMES.metrics
@@ -11,5 +9,3 @@ export const SUPPORTABILITY_METRIC = 'sm'
11
9
  export const CUSTOM_METRIC = 'cm'
12
10
  export const SUPPORTABILITY_METRIC_CHANNEL = 'storeSupportabilityMetrics'
13
11
  export const CUSTOM_METRIC_CHANNEL = 'storeEventMetrics'
14
-
15
- export const WATCHABLE_WEB_SOCKET_EVENTS = ['new', 'send', 'close', ADD_EVENT_LISTENER_TAG]
@@ -8,23 +8,13 @@ import { handle } from '../../../common/event-emitter/handle'
8
8
  import { InstrumentBase } from '../../utils/instrument-base'
9
9
  import {
10
10
  FEATURE_NAME,
11
- // WATCHABLE_WEB_SOCKET_EVENTS,
12
11
  SUPPORTABILITY_METRIC_CHANNEL
13
12
  } from '../constants'
14
- // import { handle } from '../../../common/event-emitter/handle'
15
- // import { WEBSOCKET_TAG, wrapWebSocket } from '../../../common/wrap/wrap-websocket'
16
13
 
17
14
  export class Instrument extends InstrumentBase {
18
15
  static featureName = FEATURE_NAME
19
16
  constructor (agentRef) {
20
17
  super(agentRef, FEATURE_NAME)
21
- // wrapWebSocket(this.ee) - feb'25 : removing wrapping again to avoid integration issues
22
-
23
- // WATCHABLE_WEB_SOCKET_EVENTS.forEach((suffix) => {
24
- // this.ee.on(WEBSOCKET_TAG + suffix, (...args) => {
25
- // handle('buffered-' + WEBSOCKET_TAG + suffix, [...args], undefined, this.featureName, this.ee)
26
- // })
27
- // })
28
18
 
29
19
  if (isBrowserScope) {
30
20
  document.addEventListener('securitypolicyviolation', (e) => {
@@ -3,8 +3,7 @@
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
5
  import { handle } from '../../../common/event-emitter/handle'
6
- import { subscribeToVisibilityChange } from '../../../common/window/page-visibility'
7
- import { windowAddEventListener } from '../../../common/event-listener/event-listener-opts'
6
+ import { subscribeToPageUnload, subscribeToVisibilityChange } from '../../../common/window/page-visibility'
8
7
  import { InstrumentBase } from '../../utils/instrument-base'
9
8
  import { FEATURE_NAME } from '../constants'
10
9
  import { isBrowserScope } from '../../../common/constants/runtime'
@@ -20,7 +19,7 @@ export class Instrument extends InstrumentBase {
20
19
  subscribeToVisibilityChange(() => handle('docHidden', [now()], undefined, FEATURE_NAME, this.ee), true)
21
20
 
22
21
  // Window fires its pagehide event (typically on navigation--this occurrence is a *subset* of vis change); don't defer this unless it's guarantee it cannot happen before load(?)
23
- windowAddEventListener('pagehide', () => handle('winPagehide', [now()], undefined, FEATURE_NAME, this.ee))
22
+ subscribeToPageUnload(() => handle('winPagehide', [now()], undefined, FEATURE_NAME, this.ee))
24
23
 
25
24
  this.importAggregator(agentRef, () => import(/* webpackChunkName: "page_view_timing-aggregate" */ '../aggregate'))
26
25
  }
@@ -1,39 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.handleWebsocketEvents = handleWebsocketEvents;
7
- var _dataSize = require("../../../common/util/data-size");
8
- var _text = require("../../../common/util/text");
9
- var _wrapWebsocket = require("../../../common/wrap/wrap-websocket");
10
- /**
11
- * Copyright 2020-2025 New Relic, Inc. All rights reserved.
12
- * SPDX-License-Identifier: Apache-2.0
13
- */
14
-
15
- /**
16
- * A SM handler for web socket events, which converts them to a shape suitable for SMs and reports them.
17
- * @param {Function} reporter a function that reports data as a supportability metric
18
- * @param {string} tag the unique tag to assign to the sm
19
- * @param {number} timestamp ms from page origin
20
- * @param {number} timeSinceInit ms from class init
21
- * @param {boolean} isLoaded whether the even was observed before the page load event
22
- * @param {string} socketId a unique id assigned to the observed socket
23
- * @param {*} data the data reported alongside the socket event
24
- */
25
- function handleWebsocketEvents(reporter, tag, timestamp, timeSinceInit, isLoaded, socketId, data) {
26
- // socketId is unused in the SMs
27
- const useDataType = tag === _wrapWebsocket.ADD_EVENT_LISTENER_TAG;
28
- let metricTag = (0, _text.toTitleCase)(useDataType ? data.eventType : tag);
29
- if (metricTag === 'Close') {
30
- if (data?.event.code === 1000 || data?.event.wasClean) metricTag += '-Clean';else metricTag += '-Dirty';
31
- }
32
- const bytes = metricTag === 'Message' && (0, _dataSize.dataSize)(data?.event?.data) || metricTag === 'Send' && (0, _dataSize.dataSize)(data);
33
- reporter(buildSMTag(metricTag, 'Ms'), timestamp);
34
- reporter(buildSMTag(metricTag, 'MsSinceClassInit'), timeSinceInit);
35
- if (bytes) reporter(buildSMTag(metricTag, 'Bytes'), bytes);
36
- }
37
- function buildSMTag(tag, category) {
38
- return 'WebSocket/' + tag + '/' + category;
39
- }