Skip to content

Commit

Permalink
feat: Websocket communication between k6 Studio and browser (#382)
Browse files Browse the repository at this point in the history
  • Loading branch information
allansson authored Dec 17, 2024
1 parent 0b4e704 commit 95e17e8
Show file tree
Hide file tree
Showing 15 changed files with 266 additions and 73 deletions.
75 changes: 75 additions & 0 deletions extension/src/background.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,80 @@
import { MessageEnvelope, MessagePayload } from '@/services/browser/schemas'
import { browser } from 'webextension-polyfill-ts'

let socket: WebSocket | null = null
let buffer: MessageEnvelope[] = []

function send(message: MessagePayload) {
const envelope: MessageEnvelope = {
messageId: crypto.randomUUID(),
payload: message,
}

if (socket) {
socket.send(JSON.stringify(envelope))
} else {
buffer.push(envelope)
}
}

function flush(socket: WebSocket) {
for (const message of buffer) {
socket.send(JSON.stringify(message))
}

buffer = []
}

function reconnect() {
socket?.close()
socket = null

setTimeout(() => {
connect()
}, 1000)
}

function connect() {
const ws = new WebSocket('ws://localhost:7554')

ws.onopen = () => {
console.log('Connected to server...')

socket = ws

flush(ws)
}

ws.onerror = (err) => {
console.log('Connection error...', err)

reconnect()
}

ws.onclose = () => {
console.log('Connection closed...')

reconnect()
}
}

setInterval(() => {
send({
type: 'events-captured',
events: [
{
eventId: crypto.randomUUID(),
timestamp: Date.now(),
type: 'dummy',
selector: 'button',
message: 'Clicked button',
},
],
})
}, 5000)

connect()

browser.runtime.onInstalled.addListener(() => {
console.log('Extension installed...')
})
96 changes: 40 additions & 56 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@types/node-forge": "^1.3.11",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/ws": "^8.5.13",
"@typescript-eslint/eslint-plugin": "^7.16.1",
"@typescript-eslint/parser": "^7.16.1",
"electron": "30.0.8",
Expand Down Expand Up @@ -106,6 +107,7 @@
"tree-kill": "^1.2.2",
"update-electron-app": "^3.0.0",
"webextension-polyfill-ts": "^0.26.0",
"ws": "^8.18.0",
"zod": "^3.23.8",
"zustand": "^4.5.3"
},
Expand Down
11 changes: 9 additions & 2 deletions src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { mkdtemp } from 'fs/promises'
import path from 'path'
import os from 'os'
import { appSettings } from './main'
import { launchBrowserServer } from './services/browser/server'

const createUserDataDir = async () => {
return mkdtemp(path.join(os.tmpdir(), 'k6-studio-'))
Expand Down Expand Up @@ -59,7 +60,13 @@ export const launchBrowser = async (
const extensionPath = getExtensionPath()
console.info(`extension path: ${extensionPath}`)

const sendBrowserClosedEvent = (): Promise<void> => {
const disposeWebSockerServer = appSettings.recorder.enableBrowserRecorder
? launchBrowserServer(browserWindow)
: () => {}

const handleBrowserClose = (): Promise<void> => {
disposeWebSockerServer()

// we send the browser:stopped event when the browser is closed
// NOTE: on macos pressing the X button does not close the application so it won't be fired
browserWindow.webContents.send('browser:closed')
Expand Down Expand Up @@ -87,6 +94,6 @@ export const launchBrowser = async (
disableChromeOptimizations,
url ?? '',
],
onExit: sendBrowserClosedEvent,
onExit: handleBrowserClose,
})
}
16 changes: 16 additions & 0 deletions src/hooks/useListenBrowserEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { BrowserEvent } from '@/schemas/recording'
import { useEffect, useState } from 'react'

export function useListenBrowserEvent() {
const [events, setEvents] = useState<BrowserEvent[]>([])

useEffect(() => {
return window.studio.browser.onBrowserEvent((events: BrowserEvent[]) => {
console.log('Received browser events', events)

setEvents((prevEvents) => [...prevEvents, ...events])
})
}, [])

return events
}
4 changes: 4 additions & 0 deletions src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { HarFile } from './types/har'
import { GeneratorFile } from './types/generator'
import { AddToastPayload } from './types/toast'
import { AppSettings } from './types/settings'
import { BrowserEvent } from './schemas/recording'

interface GetFilesResponse {
recordings: string[]
Expand Down Expand Up @@ -55,6 +56,9 @@ const browser = {
openExternalLink: (url: string) => {
return ipcRenderer.invoke('browser:open:external:link', url)
},
onBrowserEvent: (callback: (event: BrowserEvent[]) => void) => {
return createListener('browser:event', callback)
},
} as const

const script = {
Expand Down
1 change: 1 addition & 0 deletions src/schemas/recording/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './v1/browser'
14 changes: 14 additions & 0 deletions src/schemas/recording/v1/browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { z } from 'zod'

const DummyEvent = z.object({
eventId: z.string(),
timestamp: z.number(),
type: z.literal('dummy'),
selector: z.string(),
message: z.string(),
})

export const BrowserEventSchema = DummyEvent

export type DummyEvent = z.infer<typeof DummyEvent>
export type BrowserEvent = z.infer<typeof BrowserEventSchema>
18 changes: 18 additions & 0 deletions src/services/browser/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { BrowserEventSchema } from '@/schemas/recording'
import { z } from 'zod'

export const EventsCapturedMessage = z.object({
type: z.literal('events-captured'),
events: z.array(BrowserEventSchema),
})

export const MessagePayload = EventsCapturedMessage

export const MessageEnvelope = z.object({
messageId: z.string(),
payload: MessagePayload,
})

export type EventsCapturedMessage = z.infer<typeof EventsCapturedMessage>
export type MessagePayload = z.infer<typeof MessagePayload>
export type MessageEnvelope = z.infer<typeof MessageEnvelope>
Loading

0 comments on commit 95e17e8

Please sign in to comment.