Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f5c813a
first iteration of timeline markers for actions
allansson Feb 13, 2026
de93f4e
additional timeline work
allansson Feb 13, 2026
1b302c1
use absolute positioning for chapters
allansson Feb 13, 2026
30c2e5b
visualize actions as lanes
allansson Feb 13, 2026
cdf4d04
additional comments + add back thumb
allansson Feb 13, 2026
ecc4edd
only show hovered action in tooltip
allansson Feb 13, 2026
8465bb1
set max lane height and allow hover while running
allansson Feb 13, 2026
78371a1
set min-height on timeline
allansson Feb 16, 2026
597216e
Merge branch 'main' into feat/timeline-markers-for-actions
allansson Feb 18, 2026
822e69d
assume only two concurrent tasks by default
allansson Feb 18, 2026
856a286
safety commit
allansson Feb 25, 2026
5182a7a
minor style tweaks
allansson Mar 10, 2026
e8b9a7e
improve dragging thumb when over action
allansson Mar 10, 2026
8141212
improved styles
allansson Mar 10, 2026
4d17ec3
improve readability of timeline
allansson Mar 10, 2026
2390ecb
more style tweaks
allansson Mar 10, 2026
03f8b17
don't allocate new lanes when lanes above are free
allansson Mar 11, 2026
ea78eb7
use patch-package to add stopLive to rrweb
allansson Mar 12, 2026
6c02d07
Merge branch 'main' into feat/timeline-markers-for-actions
allansson Apr 7, 2026
febdd4c
prevent actions from rendering outside timeline
allansson Apr 7, 2026
4969d99
stopLive before liveMode config
allansson Apr 7, 2026
35b2ad8
rewrite how the timeline works
allansson Apr 9, 2026
8ea6d55
simplified the segment pos/size calculations
allansson Apr 9, 2026
3dd7116
fix action-end events not being added
allansson Apr 9, 2026
0edac21
remove unused export
allansson Apr 9, 2026
ed668e4
emit stopped event only once
allansson Apr 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 158 additions & 35 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
},
"scripts": {
"preinstall": "node install-k6.js",
"postinstall": "patch-package",
"start": "electron-forge start -- --remote-debugging-port=9223",
"start:extension": "STANDALONE_EXTENSION=true vite --config vite.extension.config.mts",
"watch:extension": "vite --config vite.extension.config.mts",
Expand Down Expand Up @@ -68,6 +69,7 @@
"husky": "^9.0.11",
"jsdom": "^25.0.0",
"lint-staged": "^15.2.5",
"patch-package": "^8.0.1",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"vite": "^5.4.21",
Expand Down
71 changes: 71 additions & 0 deletions patches/rrweb+2.0.0-alpha.18.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
diff --git a/node_modules/rrweb/dist/rrweb.cjs b/node_modules/rrweb/dist/rrweb.cjs
index d455aa0..86e7de1 100644
--- a/node_modules/rrweb/dist/rrweb.cjs
+++ b/node_modules/rrweb/dist/rrweb.cjs
@@ -14576,6 +14576,10 @@ function createPlayerService(context, { getCastFn, applyEventsSynchronously, emi
},
live: {
on: {
+ STOP_LIVE: {
+ target: "paused",
+ actions: ["pause"]
+ },
ADD_EVENT: {
target: "live",
actions: ["addEvent"]
@@ -15702,6 +15706,9 @@ class Replayer {
startLive(baselineTime) {
this.service.send({ type: "TO_LIVE", payload: { baselineTime } });
}
+ stopLive() {
+ this.service.send({ type: "STOP_LIVE" });
+ }
addEvent(rawEvent) {
const event = this.config.unpackFn ? this.config.unpackFn(rawEvent) : rawEvent;
if (indicatesTouchDevice(event)) {
diff --git a/node_modules/rrweb/dist/rrweb.d.ts b/node_modules/rrweb/dist/rrweb.d.ts
index 95305d6..66540cb 100644
--- a/node_modules/rrweb/dist/rrweb.d.ts
+++ b/node_modules/rrweb/dist/rrweb.d.ts
@@ -182,6 +182,8 @@ declare type PlayerEvent = {
};
} | {
type: 'PAUSE';
+} | {
+ type: 'STOP_LIVE';
} | {
type: 'TO_LIVE';
payload: {
@@ -299,6 +301,7 @@ export declare class Replayer {
resume(timeOffset?: number): void;
destroy(): void;
startLive(baselineTime?: number): void;
+ stopLive(): void;
addEvent(rawEvent: eventWithTime | string): void;
enableInteract(): void;
disableInteract(): void;
diff --git a/node_modules/rrweb/dist/rrweb.js b/node_modules/rrweb/dist/rrweb.js
index 5d38597..13a9d10 100644
--- a/node_modules/rrweb/dist/rrweb.js
+++ b/node_modules/rrweb/dist/rrweb.js
@@ -14574,6 +14574,10 @@ function createPlayerService(context, { getCastFn, applyEventsSynchronously, emi
},
live: {
on: {
+ STOP_LIVE: {
+ target: 'paused',
+ actions: ["pause"]
+ },
ADD_EVENT: {
target: "live",
actions: ["addEvent"]
@@ -15700,6 +15704,9 @@ class Replayer {
startLive(baselineTime) {
this.service.send({ type: "TO_LIVE", payload: { baselineTime } });
}
+ stopLive() {
+ this.service.send({ type: "STOP_LIVE" });
+ }
addEvent(rawEvent) {
const event = this.config.unpackFn ? this.config.unpackFn(rawEvent) : rawEvent;
if (indicatesTouchDevice(event)) {
5 changes: 1 addition & 4 deletions src/handlers/script/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export function initialize() {
})
})

ipcMain.on(ScriptHandler.Stop, (event) => {
ipcMain.on(ScriptHandler.Stop, () => {
console.info(`${ScriptHandler.Stop} event received`)
if (currentTestRun) {
currentTestRun.stop().catch((error) => {
Expand All @@ -92,9 +92,6 @@ export function initialize() {

currentTestRun = null
}

const browserWindow = browserWindowFromEvent(event)
browserWindow.webContents.send(ScriptHandler.Stopped)
})

ipcMain.handle(
Expand Down
53 changes: 0 additions & 53 deletions src/hooks/useBrowserActions.ts

This file was deleted.

126 changes: 119 additions & 7 deletions src/hooks/useBrowserSession.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,131 @@
import { useCallback, useEffect, useState } from 'react'

import { BrowserReplayEvent } from '@/main/runner/schema'
import { createReplayEvent } from '@/main/runner/rrweb'
import { BrowserActionEvent, BrowserReplayEvent } from '@/main/runner/schema'

export function useBrowserReplay() {
const [browserReplay, setBrowserReplay] = useState<BrowserReplayEvent[]>([])
interface BrowserSession {
actions: BrowserActionEvent[]
replay: BrowserReplayEvent[]
}

export function useBrowserSession() {
const [browserSession, setBrowserSession] = useState<BrowserSession>({
actions: [],
replay: [],
})

useEffect(() => {
return window.studio.script.onBrowserAction((event) => {
if (event.type === 'begin') {
setBrowserSession((session) => ({
...session,
actions: [...session.actions, event],
replay: [
...session.replay,
createReplayEvent({
tag: 'action-begin',
payload: {
actionId: event.eventId,
},
timestamp: event.timestamp.started,
}),
],
}))

return
}

setBrowserSession((session) => ({
...session,
actions: session.actions.map((action) =>
action.eventId === event.eventId ? event : action
),
replay: [
...session.replay,
createReplayEvent({
tag: 'action-end',
payload: {
actionId: event.eventId,
},
timestamp: event.timestamp.ended,
}),
],
}))
})
}, [])

useEffect(() => {
return window.studio.script.onBrowserReplay((events) => {
setBrowserReplay((existing) => [...existing, ...events])
setBrowserSession((session) => ({
...session,
replay: [...session.replay, ...events],
}))
})
}, [])

const resetBrowserReplay = useCallback(() => {
setBrowserReplay([])
useEffect(() => {
return window.studio.script.onScriptStopped(() => {
setBrowserSession((session) => {
const now = Date.now()

// Abort all actions that were running when the script stopped.
const actions: BrowserActionEvent[] = session.actions.map((action) => {
if (action.type !== 'begin') {
return action
}

return {
...action,
type: 'end',
timestamp: {
...action.timestamp,
ended: now,
},
result: {
type: 'aborted',
},
}
})

// Insert action end events for all actions that were running when the script stopped.
const actionEnds = session.actions
.filter((action) => action.type === 'begin')
.map((action) =>
createReplayEvent({
tag: 'action-end',
payload: {
actionId: action.eventId,
},
timestamp: now,
})
)

return {
...session,
actions,
replay: [
...session.replay,
...actionEnds,
createReplayEvent({
tag: 'recording-end',
payload: {},
timestamp: now,
}),
],
}
})
})
}, [])

const resetBrowserSession = useCallback(() => {
setBrowserSession({
actions: [],
replay: [],
})
}, [])

return { browserReplay, resetBrowserReplay }
return {
browserSession,
resetBrowserSession,
}
}
31 changes: 27 additions & 4 deletions src/main/runner/rrweb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,25 @@ const PageStartEventSchema = z.object({
}),
})

const ActionBeginEventSchema = z.object({
tag: z.literal('action-begin'),
payload: z.object({
actionId: z.string(),
}),
})

const ActionEndEventSchema = z.object({
tag: z.literal('action-end'),
payload: z.object({
actionId: z.string(),
}),
})

const CustomReplayEventSchema = z.discriminatedUnion('tag', [
RecordingEndEventSchema,
PageStartEventSchema,
ActionBeginEventSchema,
ActionEndEventSchema,
])

const RrwebCustomEventSchema = z.object({
Expand All @@ -42,13 +58,20 @@ export function parseReplayEvent(event: unknown) {
return RrwebCustomEventSchema.parse(event)
}

export function createReplayEvent<T extends CustomReplayEvent['tag']>(
tag: T,
interface CreateReplayEventOptions<T extends keyof CustomReplayEventMap> {
tag: T
payload: CustomReplayEventMap[T]['payload']
): BrowserReplayEvent {
timestamp?: number
}

export function createReplayEvent<T extends keyof CustomReplayEventMap>({
tag,
payload,
timestamp,
}: CreateReplayEventOptions<T>): BrowserReplayEvent {
return {
type: EventType.Custom,
timestamp: Date.now(),
timestamp: timestamp ?? Date.now(),
data: {
tag,
payload,
Expand Down
2 changes: 2 additions & 0 deletions src/main/runner/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,9 @@ export const ActionBeginEventSchema = ActionEventSchemaBase.extend({
type: z.literal('begin'),
timestamp: z.object({
started: z.number(),
ended: z.undefined().optional(),
}),
result: z.undefined().optional(),
})

export const ActionSuccessSchema = z.object({
Expand Down
9 changes: 0 additions & 9 deletions src/main/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { ArchiveError, K6Client } from '@/utils/k6/client'
import { createTrackingServer } from '@/utils/k6/tracking'

import { instrumentScriptFromPath as instrumentScriptFromPath } from './runner/instrumentation'
import { createReplayEvent } from './runner/rrweb'

export type K6Process = ChildProcessWithoutNullStreams

Expand Down Expand Up @@ -116,21 +115,13 @@ export const runScript = async ({
browserWindow.webContents.send(ScriptHandler.Finished, result)
})

testRun.on('abort', () => {
browserWindow.webContents.send(ScriptHandler.Stopped)
})

testRun.on('error', (error) => {
log.error(error)

browserWindow.webContents.send(ScriptHandler.Failed)
})

testRun.on('stop', () => {
browserWindow.webContents.send(ScriptHandler.BrowserReplay, [
createReplayEvent('recording-end', {}),
])

browserWindow.webContents.send(ScriptHandler.Stopped)

trackingServer?.dispose()
Expand Down
Loading