Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Re-implement wrap-websocket into agent
Browse files Browse the repository at this point in the history
ellisong committed Jan 23, 2025
1 parent 7c309f7 commit f5e39c5
Showing 11 changed files with 187 additions and 98 deletions.
2 changes: 1 addition & 1 deletion src/common/wrap/wrap-function.js
Original file line number Diff line number Diff line change
@@ -182,7 +182,7 @@ function report (args, emitter) {
* Defaults to the global event emitter.
* @returns {object} - The destination founction or object with copied properties.
*/
function copy (from, to, emitter) {
export function copy (from, to, emitter) {
if (Object.defineProperty && Object.keys) {
// Create accessors that proxy to actual function
try {
64 changes: 24 additions & 40 deletions src/common/wrap/wrap-websocket.js
Original file line number Diff line number Diff line change
@@ -23,51 +23,35 @@ export function wrapWebSocket (sharedEE) {
}
}

Object.defineProperty(WrappedWebSocket, 'name', {
value: 'WebSocket'
})

function WrappedWebSocket () {
const ws = new originals.WS(...arguments)
const socketId = generateRandomHexString(6)
const report = reporter(socketId)
report('new')

const events = ['message', 'error', 'open', 'close']
/** add event listeners */
events.forEach(evt => {
ws.addEventListener(evt, function (e) {
report(ADD_EVENT_LISTENER_TAG, { eventType: evt, event: e })
class WrappedWebSocket extends WebSocket {
static name = 'WebSocket'

constructor (...args) {
super(...args)
const socketId = generateRandomHexString(6)
this.report = reporter(socketId)
this.report('new')

const events = ['message', 'error', 'open', 'close']
/** add event listeners */
events.forEach(evt => {
this.addEventListener(evt, function (e) {
this.report(ADD_EVENT_LISTENER_TAG, { eventType: evt, event: e })
})
})
})

/** could also observe the on-events for runtime processing, but not implemented yet */

/** observe the static method send, but noteably not close, as that is currently observed with the event listener */
;['send'].forEach(wrapStaticProperty)
}

function wrapStaticProperty (prop) {
const originalProp = ws[prop]
if (originalProp) {
Object.defineProperty(proxiedProp, 'name', {
value: prop
})
function proxiedProp () {
report(prop, ...arguments)
try {
return originalProp.apply(this, arguments)
} catch (err) {
report(prop + '-err', ...arguments)
// rethrow error so we don't effect execution by observing.
throw err
}
}
ws[prop] = proxiedProp
send (...args) {
this.report('send', ...args)
try {
return super.send(...args)
} catch (err) {
this.report('send-err', ...args)
throw err
}
}

return ws
}

globalScope.WebSocket = WrappedWebSocket
return sharedEE
}
16 changes: 8 additions & 8 deletions src/features/metrics/aggregate/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { registerHandler } from '../../../common/event-emitter/register-handler'
import { FEATURE_NAME, SUPPORTABILITY_METRIC, CUSTOM_METRIC, SUPPORTABILITY_METRIC_CHANNEL, CUSTOM_METRIC_CHANNEL/*, WATCHABLE_WEB_SOCKET_EVENTS */ } from '../constants'
import { FEATURE_NAME, SUPPORTABILITY_METRIC, CUSTOM_METRIC, SUPPORTABILITY_METRIC_CHANNEL, CUSTOM_METRIC_CHANNEL, WATCHABLE_WEB_SOCKET_EVENTS } from '../constants'
import { getFrameworks } from './framework-detection'
import { isFileProtocol } from '../../../common/url/protocol'
import { onDOMContentLoaded } from '../../../common/window/load'
import { windowAddEventListener } from '../../../common/event-listener/event-listener-opts'
import { isBrowserScope, isWorkerScope } from '../../../common/constants/runtime'
import { AggregateBase } from '../../utils/aggregate-base'
import { isIFrameWindow } from '../../../common/dom/iframe'
// import { WEBSOCKET_TAG } from '../../../common/wrap/wrap-websocket'
// import { handleWebsocketEvents } from './websocket-detection'
import { WEBSOCKET_TAG } from '../../../common/wrap/wrap-websocket'
import { handleWebsocketEvents } from './websocket-detection'

export class Aggregate extends AggregateBase {
static featureName = FEATURE_NAME
@@ -112,11 +112,11 @@ export class Aggregate extends AggregateBase {
mo.observe(window.document.body, { childList: true, subtree: true })
}

// WATCHABLE_WEB_SOCKET_EVENTS.forEach(tag => {
// registerHandler('buffered-' + WEBSOCKET_TAG + tag, (...args) => {
// handleWebsocketEvents(this.storeSupportabilityMetrics.bind(this), tag, ...args)
// }, this.featureName, this.ee)
// })
WATCHABLE_WEB_SOCKET_EVENTS.forEach(tag => {
registerHandler('buffered-' + WEBSOCKET_TAG + tag, (...args) => {
handleWebsocketEvents(this.storeSupportabilityMetrics.bind(this), tag, ...args)
}, this.featureName, this.ee)
})
}

eachSessionChecks () {
4 changes: 2 additions & 2 deletions src/features/metrics/constants.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// import { ADD_EVENT_LISTENER_TAG } from '../../common/wrap/wrap-websocket'
import { ADD_EVENT_LISTENER_TAG } from '../../common/wrap/wrap-websocket'
import { FEATURE_NAMES } from '../../loaders/features/features'

export const FEATURE_NAME = FEATURE_NAMES.metrics
@@ -7,4 +7,4 @@ export const CUSTOM_METRIC = 'cm'
export const SUPPORTABILITY_METRIC_CHANNEL = 'storeSupportabilityMetrics'
export const CUSTOM_METRIC_CHANNEL = 'storeEventMetrics'

// export const WATCHABLE_WEB_SOCKET_EVENTS = ['new', 'send', 'close', ADD_EVENT_LISTENER_TAG]
export const WATCHABLE_WEB_SOCKET_EVENTS = ['new', 'send', 'close', ADD_EVENT_LISTENER_TAG]
18 changes: 9 additions & 9 deletions src/features/metrics/instrument/index.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
// import { handle } from '../../../common/event-emitter/handle'
// import { WEBSOCKET_TAG, wrapWebSocket } from '../../../common/wrap/wrap-websocket'
import { handle } from '../../../common/event-emitter/handle'
import { WEBSOCKET_TAG, wrapWebSocket } from '../../../common/wrap/wrap-websocket'
import { InstrumentBase } from '../../utils/instrument-base'
import { FEATURE_NAME/*, WATCHABLE_WEB_SOCKET_EVENTS */ } from '../constants'
import { FEATURE_NAME, WATCHABLE_WEB_SOCKET_EVENTS } from '../constants'

export class Instrument extends InstrumentBase {
static featureName = FEATURE_NAME
constructor (agentRef, auto = true) {
super(agentRef, FEATURE_NAME, auto)
// wrapWebSocket(this.ee)
wrapWebSocket(this.ee)

// WATCHABLE_WEB_SOCKET_EVENTS.forEach((suffix) => {
// this.ee.on(WEBSOCKET_TAG + suffix, (...args) => {
// handle('buffered-' + WEBSOCKET_TAG + suffix, [...args], undefined, this.featureName, this.ee)
// })
// })
WATCHABLE_WEB_SOCKET_EVENTS.forEach((suffix) => {
this.ee.on(WEBSOCKET_TAG + suffix, (...args) => {
handle('buffered-' + WEBSOCKET_TAG + suffix, [...args], undefined, this.featureName, this.ee)
})
})

this.importAggregator(agentRef)
}
6 changes: 6 additions & 0 deletions tests/components/wrappings/wrap-websocket.test.js
Original file line number Diff line number Diff line change
@@ -29,6 +29,12 @@ describe('wrap-websocket', () => {
expect(ws.protocol).toEqual('')
expect(ws.readyState).toEqual(0)
expect(ws.url).toEqual('ws://foo.com/websocket')

expect(WebSocket.length).not.toBeUndefined()
expect(WebSocket.CONNECTING).toEqual(0)
expect(WebSocket.OPEN).toEqual(1)
expect(WebSocket.CLOSING).toEqual(2)
expect(WebSocket.CLOSED).toEqual(3)
})

it('should not run if no WS global', async () => {
102 changes: 67 additions & 35 deletions tests/specs/websockets/metrics.e2e.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,72 @@
// const { notIOS, notSafari } = require('../../../tools/browser-matcher/common-matchers.mjs')
// const { testSupportMetricsRequest } = require('../../../tools/testing-server/utils/expect-tests')
const { notIOS, notSafari } = require('../../../tools/browser-matcher/common-matchers.mjs')
const { testSupportMetricsRequest, testErrorsRequest } = require('../../../tools/testing-server/utils/expect-tests')

describe('WebSocket supportability metrics', () => {
/** Safari and iOS safari are blocked from connecting to the websocket protocol on LT, which throws socket errors instead of connecting and capturing the expected payloads.
* validated that this works locally for these envs */
// it.withBrowsersMatching([notSafari, notIOS])('should capture expected SMs', async () => {
// const supportabilityMetricsRequest = await browser.testHandle.createNetworkCaptures('bamServer', { test: testSupportMetricsRequest })
// const url = await browser.testHandle.assetURL('websockets.html')

// await browser.url(url)
// .then(() => browser.waitForAgentLoad())
// .then(() => browser.refresh())

// const [sms] = await supportabilityMetricsRequest.waitForResult({ totalCount: 1 })
// const smPayload = sms.request.body.sm
// const smTags = ['New', 'Open', 'Send', 'Message', 'Close-Clean']

// smTags.forEach(expectedSm => {
// const ms = smPayload.find(sm => sm.params.name === `WebSocket/${expectedSm}/Ms`)
// const msSinceClassInit = smPayload.find(sm => sm.params.name === `WebSocket/${expectedSm}/MsSinceClassInit`)
// const bytes = smPayload.find(sm => sm.params.name === `WebSocket/${expectedSm}/Bytes`)

// expect(ms).toBeTruthy()
// expect(ms.stats.t).toBeGreaterThan(0)
// expect(ms.stats.c).toEqual(2)

// expect(msSinceClassInit).toBeTruthy()
// if (expectedSm === 'New') expect(msSinceClassInit.stats.t).toBeLessThanOrEqual(1)
// else expect(msSinceClassInit.stats.t).toBeGreaterThan(0)
// expect(msSinceClassInit.stats.c).toEqual(2)

// if (['Send', 'Message'].includes(expectedSm)) {
// expect(bytes).toBeTruthy()
// if (expectedSm === 'Send') expect(bytes.stats.t / bytes.stats.c).toBeGreaterThanOrEqual(8) // we are sending about 8 bytes from client to server
// if (expectedSm === 'Message') expect(bytes.stats.t / bytes.stats.c).toBeGreaterThanOrEqual(40) // we are sending about 40 bytes from server to client
// } else expect(bytes).toBeFalsy()
// })
// })
it.withBrowsersMatching([notSafari, notIOS])('should capture expected SMs', async () => {
const supportabilityMetricsRequest = await browser.testHandle.createNetworkCaptures('bamServer', { test: testSupportMetricsRequest })
const url = await browser.testHandle.assetURL('websockets.html')

await browser.url(url)
.then(() => browser.waitForAgentLoad())
.then(() => browser.refresh())

const [sms] = await supportabilityMetricsRequest.waitForResult({ totalCount: 1 })
const smPayload = sms.request.body.sm
const smTags = ['New', 'Open', 'Send', 'Message', 'Close-Clean']

smTags.forEach(expectedSm => {
const ms = smPayload.find(sm => sm.params.name === `WebSocket/${expectedSm}/Ms`)
const msSinceClassInit = smPayload.find(sm => sm.params.name === `WebSocket/${expectedSm}/MsSinceClassInit`)
const bytes = smPayload.find(sm => sm.params.name === `WebSocket/${expectedSm}/Bytes`)

expect(ms).toBeTruthy()
expect(ms.stats.t).toBeGreaterThan(0)
expect(ms.stats.c).toEqual(2)

expect(msSinceClassInit).toBeTruthy()
if (expectedSm === 'New') expect(msSinceClassInit.stats.t).toBeLessThanOrEqual(1)
else expect(msSinceClassInit.stats.t).toBeGreaterThan(0)
expect(msSinceClassInit.stats.c).toEqual(2)

if (['Send', 'Message'].includes(expectedSm)) {
expect(bytes).toBeTruthy()
if (expectedSm === 'Send') expect(bytes.stats.t / bytes.stats.c).toBeGreaterThanOrEqual(8) // we are sending about 8 bytes from client to server
if (expectedSm === 'Message') expect(bytes.stats.t / bytes.stats.c).toBeGreaterThanOrEqual(40) // we are sending about 40 bytes from server to client
} else expect(bytes).toBeFalsy()
})
})

;['robust-websocket', 'reconnecting-websocket'].forEach((thirdPartyWSWrapper) => {
it('should work with known third-party WS wrapper - ' + thirdPartyWSWrapper, async () => {
const [supportabilityMetricsRequest, errorsRequest] =
await browser.testHandle.createNetworkCaptures('bamServer', [
{ test: testSupportMetricsRequest },
{ test: testErrorsRequest }
])
const url = await browser.testHandle.assetURL(`test-builds/library-wrapper/${thirdPartyWSWrapper}.html`)

await browser.url(url)

const [errors, [sms]] = await Promise.all([
errorsRequest.waitForResult({ timeout: 10000 }),
supportabilityMetricsRequest.waitForResult({ totalCount: 1 })
])
// should not have thrown errors
expect(errors.length).toEqual(0)

const smPayload = sms.request.body.sm
const smTags = ['New', 'Open', 'Send', 'Message', 'Close-Clean']

smTags.forEach(expectedSm => {
const ms = smPayload.find(sm => sm.params.name === `WebSocket/${expectedSm}/Ms`)
const msSinceClassInit = smPayload.find(sm => sm.params.name === `WebSocket/${expectedSm}/MsSinceClassInit`)
const bytes = smPayload.find(sm => sm.params.name === `WebSocket/${expectedSm}/Bytes`)
expect(ms).toBeTruthy()
expect(msSinceClassInit).toBeTruthy()
if (['Send', 'Message'].includes(expectedSm))expect(bytes).toBeTruthy()
})
})
})
})
4 changes: 3 additions & 1 deletion tools/test-builds/library-wrapper/package.json
Original file line number Diff line number Diff line change
@@ -11,6 +11,8 @@
"dependencies": {
"@apollo/client": "^3.8.8",
"@newrelic/browser-agent": "file:../../../temp/newrelic-browser-agent-1.278.3.tgz",
"graphql": "^16.8.1"
"graphql": "^16.8.1",
"reconnecting-websocket": "^4.4.0",
"robust-websocket": "^1.0.0"
}
}
25 changes: 25 additions & 0 deletions tools/test-builds/library-wrapper/src/reconnecting-websocket.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { BrowserAgent } from '@newrelic/browser-agent/loaders/browser-agent'

import ReconnectingWebSocket from 'reconnecting-websocket'

const opts = {
info: NREUM.info,
init: NREUM.init
}

new BrowserAgent(opts)

window.bamServer = NREUM.info.beacon

const rws = new ReconnectingWebSocket(`ws://${window.bamServer}/websocket`)

rws.addEventListener('open', () => {
rws.send('hello!')
rws.addEventListener('message', (message) => {
rws.close()
})

rws.addEventListener('close', function () {
window.location.reload()
})
})
26 changes: 26 additions & 0 deletions tools/test-builds/library-wrapper/src/robust-websocket.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { BrowserAgent } from '@newrelic/browser-agent/loaders/browser-agent'

import * as RobustWebSocket from 'robust-websocket'

const opts = {
info: NREUM.info,
init: NREUM.init
}

new BrowserAgent(opts)

window.bamServer = NREUM.info.beacon

var ws = new RobustWebSocket(`ws://${window.bamServer}/websocket`)

ws.addEventListener('open', function (event) {
ws.send('Hello!')
})

ws.addEventListener('message', function (event) {
ws.close()
})

ws.addEventListener('close', function () {
window.location.reload()
})
18 changes: 16 additions & 2 deletions tools/test-builds/library-wrapper/webpack.config.js
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

const isProduction = process.env.NODE_ENV === 'production'
const htmlTemplate = (script) => `<html>
const htmlTemplate = (script) => `<!DOCTYPE html>
<head>
<title>RUM Unit Test</title>
{init}
@@ -19,7 +19,9 @@ const config = [
{
cache: false,
entry: {
'apollo-client': './src/apollo-client.js'
'apollo-client': './src/apollo-client.js',
'reconnecting-websocket': './src/reconnecting-websocket.js',
'robust-websocket': './src/robust-websocket.js'
},
output: {
path: path.resolve(__dirname, '../../../tests/assets/test-builds/library-wrapper')
@@ -66,6 +68,18 @@ const config = [
minify: false,
inject: false,
templateContent: htmlTemplate('apollo-client')
}),
new HtmlWebpackPlugin({
filename: 'reconnecting-websocket.html',
minify: false,
inject: false,
templateContent: htmlTemplate('reconnecting-websocket')
}),
new HtmlWebpackPlugin({
filename: 'robust-websocket.html',
minify: false,
inject: false,
templateContent: htmlTemplate('robust-websocket')
})
]
}

0 comments on commit f5e39c5

Please sign in to comment.