Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
155 commits
Select commit Hold shift + click to select a range
7bdc7dc
feat: add recording dashboard UI and nav element migration
Mar 22, 2026
2dbd5a1
feat: add recording dashboard component
Mar 22, 2026
b788500
feat: rename recording to socket profiler
Mar 24, 2026
3117dae
feat: add recording and trace tables with models
Mar 28, 2026
be58e84
feat: update socket profiler dashboard with subscribeTable and correc…
Mar 28, 2026
de3f283
feat: implement socket recorder with onAny listeners and real-time tr…
Mar 30, 2026
6e19b77
feat: add deleted and deletedAt columns to trace table
Mar 30, 2026
add5478
fix: format timestamps, remove stop button from table rows
Apr 5, 2026
535a693
feat: add persistent recording bar to dashboard
Apr 6, 2026
d98c81d
feat: add save recording modal with trace selection
Apr 7, 2026
862885c
fix: recordings persist after reload, set deleted false on create
Apr 7, 2026
7bb50f1
refactor: remove catch-all listeners by reference instead of nuking all
Apr 8, 2026
becee70
fix: default deleted to false on recording and trace models
Apr 8, 2026
c24ac5a
chore: stop tracking root logs directory
Apr 12, 2026
cf906f8
refactor: collapse trace migrations and use proper timestamps
Apr 12, 2026
daf9445
fix: set publicTable false on recording and trace, move deleted defau…
Apr 12, 2026
17c9b8d
refactor: remove redundant sendToast calls and unused return from sta…
Apr 12, 2026
a1fb6e2
refactor: use BasicButton in recording modal for behavior tracking
Apr 12, 2026
41c6a27
feat: add edit recording modal with trace pruning
Apr 16, 2026
38b0bda
feat: capture multi-user events across all connected sockets
Apr 16, 2026
c5d691c
feat: multi-user recording with participant selection and online indi…
Apr 17, 2026
b1966c3
Merge remote-tracking branch 'origin/dev' into feat-83-recording
Apr 20, 2026
6bbf384
feat: add replay engine with results modal
Apr 20, 2026
e9780eb
feat: add event exclusion filter and update modals to use BasicTable
Apr 21, 2026
ddc98ac
feat: rename 'Level' to 'Iteration' in replay UI
Apr 27, 2026
89339c0
docs: note coupling between replay HMAC and session secret
Apr 27, 2026
a5f866d
fix: recover interrupted recordings, await save operations
Apr 27, 2026
8e991d9
feat: tag traces with socketId for per-session granularity
Apr 27, 2026
2376e83
fix: correct selected count, show session count in start modal
Apr 27, 2026
ccd234a
feat: replay groups traces by socketId for per-session granularity
Apr 27, 2026
ecac28f
feat: pure session-based recording selection
May 3, 2026
cd41288
feat: persistent recording icon replaces global recording bar
May 3, 2026
dae7a5b
feat: replay configuration modal with pooled scaling
May 6, 2026
7aa5b2d
Merge branch 'dev' into feat-83-recording
May 6, 2026
d796191
fix: move Socket Profiler nav element into Manage group
May 6, 2026
8bd3b00
feat: live elapsed-time display on recording icon + restore click nav…
May 11, 2026
e7f6a7b
chore: ignore .DS_Store files
May 11, 2026
6fc56cb
fix: close mounted() method properly in Topbar.vue
May 11, 2026
58fb9c2
feat: add Time and Elapsed columns to recording traces modal
May 11, 2026
a11a8e0
style: fix Vue attribute-order lint warnings
May 12, 2026
e50f23f
chore: remove leftover debug console.log from replay worker
May 12, 2026
13e13e4
Merge remote-tracking branch 'origin/dev' into feat-83-recording
May 19, 2026
f3ff342
feat: export and import recordings as JSON
May 20, 2026
bc66320
feat: configurable ack timeout for replay
May 20, 2026
a550780
style: clean up Server.js indentation, JSDoc, and trailing whitespace
May 21, 2026
df75b95
chore: sync backend package-lock with nodemon devDependency
May 21, 2026
88f24ef
feat: auto-download replay results as JSON
May 25, 2026
acc0eb7
fix: warm-up delay before replay to avoid first-trace race
May 28, 2026
31bcbae
refactor: use BasicForm for replay config fields
May 28, 2026
5bdb0c1
feat: strip dbChanges from downloaded replay results
May 28, 2026
8c86eaa
fix: show recording icon only on sessions being recorded
Jun 2, 2026
0541846
feat: chronological sequence column in recording traces modal
Jun 2, 2026
96e6d52
fix: populate Connected time and mark current tab in session picker
Jun 2, 2026
a171e8b
feat: help tooltips for replay config fields
Jun 2, 2026
5819b40
fix: replace replay warm-up delay with server ready signal
Jun 8, 2026
f4ee14c
fix: filter stale sockets from online session list
Jun 10, 2026
8afcab4
feat: flag recording disconnected on participant drop; debounce uncap…
Jun 10, 2026
b87add7
feat: live-update session picker as connections come and go
Jun 10, 2026
00b255c
fix: include payload in getTraces so exports carry it
Jun 15, 2026
f66753d
fix: set participantSocketIds on imported recordings
Jun 15, 2026
b5a6f3c
fix: name exported recording file after the recording
Jun 15, 2026
b0e5150
Add integrity guard to recording export
Jun 15, 2026
f03d43f
Add validators to recording and trace models
Jun 15, 2026
b62cdd3
Document trace direction boolean in migration
Jun 15, 2026
f26d260
Require admin for start/stop recording
Jun 15, 2026
1f6e5c4
Avoid N+1 query when building replay session pool
Jun 15, 2026
c5eecef
Log session cleanup failures instead of swallowing
Jun 15, 2026
ea4f4a1
Fix direction arrow in recording trace label
Jun 15, 2026
3e41e64
Fix crash in unsubscribeAppData on unknown subscription id
Jun 15, 2026
3518f34
Add progress bar to replay runs
Jun 15, 2026
e8513b2
Recorder: per-socket recordings instead of one server-wide recording
Jun 22, 2026
6f6fd29
Socket profiler: broadcast stopped recordings + frontend per-socket stop
Jun 22, 2026
9065276
Socket profiler: fix recording icon for concurrent per-socket recordings
Jun 22, 2026
d33102e
Replay results: collapsible iteration overview + exclude recorder con…
Jun 23, 2026
45bafa0
Recorder: require admin for trace and session read handlers
Jun 23, 2026
502eb00
Replay results: two-level table navigation (iterations -> sessions ->…
Jun 29, 2026
a0f5f10
Replay progress: per-trace granularity instead of per-iteration
Jun 29, 2026
a66a121
Replay progress: throttle emits via lodash
Jul 5, 2026
0c27fc2
perf: CLI load-test scaffold — auth, recording resolution, regression…
Jul 5, 2026
9c6e82c
Replay engine: latency-threshold stop + single-level mode
Jul 5, 2026
98b1582
perf: add ramp, ceiling, and inspect modes
Jul 5, 2026
695994c
perf: add soak mode
Jul 6, 2026
255c49b
perf metrics: expose backend health + pg-stats via socket handlers
Jul 6, 2026
392dccb
perf metrics: sampler + wire into soak
Jul 6, 2026
7aede14
perf metrics: add DB pool-waiting stop to ceiling
Jul 6, 2026
263c045
perf regression: folder suite support + per-story verdict
Jul 7, 2026
e4d61a2
Merge remote-tracking branch 'origin/dev' into feat-83-recording
Jul 7, 2026
12ff306
perf: per-trace-type diagnostic stats across all modes
Jul 8, 2026
b0a6faa
perf: memory-vs-trace correlation in soak + vitals in ramp
Jul 8, 2026
bd7d252
perf: surface Postgres stats in all load modes + memory in ceiling
Jul 8, 2026
e764013
perf: save results to disk (JSON + readable text) across all modes
Jul 9, 2026
a3aca96
feat: replay recordings from files without db import
Jul 12, 2026
0049745
feat: read perf admin password from flag, env, or prompt
Jul 12, 2026
823ce43
refactor: inspect shows plain trace distribution without classification
Jul 12, 2026
c2afb4a
feat: make replay target url configurable via env
Jul 12, 2026
f21c3cc
feat: report node heap alongside rss in load modes
Jul 12, 2026
8ce4263
feat: nest replay results drill-down as child modals
Jul 12, 2026
c7cf59f
refactor: address review findings in replayer
Jul 16, 2026
d35f3df
refactor: address review findings in recorder
Jul 16, 2026
da9ce48
fix: correct scope, session cleanup and secret handling in replay int…
Jul 16, 2026
167a1be
fix: restrict recording table access and correct schema definitions
Jul 16, 2026
6ba489d
fix: correct recording status validation, foreign keys and start claims
Jul 16, 2026
9a07d8d
fix: address re-review findings across recorder, replayer and replay …
Jul 17, 2026
1935b84
fix: address review findings in socket profiler frontend
Jul 18, 2026
1956b5e
fix: remove dead import path and guard file loading in perf CLI
Jul 18, 2026
0a2350b
fix: refresh recordings table on import via remount workaround
Jul 18, 2026
641a1b2
chore: untrack committed .DS_Store files and fix gitignore
Jul 18, 2026
a1102d9
fix: reject replay runs whose peak concurrency exceeds the limit
Jul 22, 2026
079eb1c
refactor: consolidate socket profiler nav migration into manage group
Jul 22, 2026
8ffda3c
fix: validate session secret before inserting session row
Jul 22, 2026
fc255a1
refactor: rename verifyAdmin to verifyAuthenticatedSession
Jul 22, 2026
4f305d6
fix: surface readable report write failures
Jul 22, 2026
e623126
fix: continue inspection when one recording fetch fails
Jul 22, 2026
d8ef2fa
fix: stop output capture on early return and key stories by stable id
Jul 22, 2026
e150a33
fix: clean up progress listener and sampler on all exit paths
Jul 22, 2026
78412cc
fix: clean up sampler and output capture on all exit paths
Jul 22, 2026
02f8a04
fix: guarantee sampler cleanup, wire pool baseline, reuse printCulprit
Jul 22, 2026
fffc0f3
fix: validate latency threshold and read ceiling exit code
Jul 22, 2026
bcf6f83
fix: reset modal open flag on hide event
Jul 22, 2026
8eb85d8
Merge remote-tracking branch 'origin/dev' into feat-83-recording
Jul 26, 2026
b42fcdf
refactor: rename loginAsAdmin to login
Aug 2, 2026
492ae87
chore: move perf scripts into perf subdirectory
Aug 2, 2026
b70c2bb
chore: restructure perf scripts into modes and utils
Aug 2, 2026
b69327f
fix: save perf results under the backend log directory
Aug 3, 2026
612d90a
docs: describe expected object contents in perf jsdoc
Aug 3, 2026
3597cad
docs: complete missing perf jsdoc params
Aug 3, 2026
728fcf7
refactor: move replay helpers into backend utils
Aug 3, 2026
e6f77b8
refactor: return trace rows directly instead of remapping
Aug 3, 2026
4bde2e6
fix: restrict session change broadcast to admins
Aug 3, 2026
c98d823
refactor: move recording recovery helpers into utils
Aug 3, 2026
e9b1413
fix: await first metric sample so pool baseline is populated
Aug 3, 2026
5d9f96b
docs: add missing perf jsdoc
Aug 3, 2026
0a7b3b6
docs: correct parseDuration return type
Aug 3, 2026
226e6b8
feat: replay regression sessions sequentially for deterministic ids
Aug 3, 2026
7467bca
fix: accept --dir as a standalone recording source in perf cli
Aug 3, 2026
c2d0197
feat: show replay progress during regression runs
Aug 8, 2026
5ab3d69
feat: replay file sessions as their recorded user
Aug 8, 2026
0c40f61
fix: report recorded user per story and hide unused maxIterations
Aug 9, 2026
4570905
refactor: move replay session helpers into backend utils
Aug 10, 2026
4bb209b
refactor: reuse a single raw recordings computed in socket profiler
Aug 10, 2026
4ba35bd
feat: import recordings through a single transactional handler
Aug 10, 2026
decbeae
fix: fail regression when a story is skipped for a missing user
Aug 10, 2026
dd646a4
fix: only echo maxIterations for modes that use it
Aug 10, 2026
67b6b08
refactor: drop duplicate recording broadcast on disconnect
Aug 10, 2026
730c34c
fix: restore buffers in trace payloads before replay
Aug 10, 2026
a295d62
fix: read session identity from this.userId when saving a study template
Aug 14, 2026
e207bad
fix: remap recorded hashes to replayed ones during replay
Aug 14, 2026
e4544e2
fix: share observed hashes across sessions and map them from recorded…
Aug 14, 2026
2fc3a3e
chore: remove hash remapping debug output
Aug 14, 2026
e9a31c5
feat: report unacknowledged traces separately from failures
Aug 14, 2026
9024c82
feat: wire up unused perf cli options
Aug 14, 2026
de2892c
test: add recorded user story regression suite
Aug 14, 2026
eda41fb
chore: mark regression stories as generated
Aug 14, 2026
355b4cf
docs: add socket profiler documentation
Aug 14, 2026
31e2f15
Merge remote-tracking branch 'origin/dev' into feat-83-recording
Aug 14, 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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ backend/sessions
backend/node_modules
backend/logs
backend/coverage
logs/

# ignore build files
dist/
Expand Down Expand Up @@ -49,4 +50,5 @@ utils/rpcs/moodleAPI/moodle_api/__pycache__
*.pyc
email-mappings.py
gitleaks-clean.sh
.mailmap
.mailmap
.DS_Store
83 changes: 83 additions & 0 deletions backend/db/migrations/20260412231117-create-recording.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
'use strict';

module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('recording', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
name: {
type: Sequelize.STRING,
allowNull: true,
},
status: {
type: Sequelize.STRING,
allowNull: false,
defaultValue: 'recording',
},
startTime: {
type: Sequelize.DATE,
allowNull: false,
},
endTime: {
type: Sequelize.DATE,
allowNull: true,
defaultValue: null,
},
// Recordings are soft-deleted only, so a user delete must never cascade
// them away. RESTRICT forces whoever adds user deletion to soft-delete
// the recordings first rather than silently destroying recorded data.
userId: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: 'user',
key: 'id',
},
onDelete: 'RESTRICT',
onUpdate: 'CASCADE',
},
participantSocketIds: {
type: Sequelize.JSONB,
allowNull: true,
defaultValue: null,
},
excludeEvents: {
type: Sequelize.JSONB,
allowNull: true,
defaultValue: null,
},
deleted: {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false,
},
deletedAt: {
type: Sequelize.DATE,
allowNull: true,
defaultValue: null,
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.fn('NOW'),
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.fn('NOW'),
},
});
// publicTable is false, so getAutoTable filters these rows by userId.
await queryInterface.addIndex('recording', ['userId'], {
name: 'recording_userId_idx',
});
},

async down(queryInterface, Sequelize) {
await queryInterface.dropTable('recording');
},
};
96 changes: 96 additions & 0 deletions backend/db/migrations/20260412231119-create-trace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
'use strict';

module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('trace', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
recordingId: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: 'recording',
key: 'id',
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
// SET NULL would leave the recording intact but silently unreplayable
// (groupTracesBySocket skips traces with no userId). RESTRICT keeps trace
// ownership honest; recordings are soft-deleted, never orphaned.
userId: {
type: Sequelize.INTEGER,
allowNull: true,
references: {
model: 'user',
key: 'id',
},
onDelete: 'RESTRICT',
onUpdate: 'CASCADE',
},
socketId: {
type: Sequelize.STRING,
allowNull: true,
},
action: {
type: Sequelize.STRING,
allowNull: false,
},
payload: {
type: Sequelize.JSONB,
allowNull: true,
},
// true = frontend -> backend (onAny), false = backend -> frontend (onAnyOutgoing)
direction: {
Comment thread
dennis-zyska marked this conversation as resolved.
type: Sequelize.BOOLEAN,
allowNull: false,
},
startTime: {
type: Sequelize.DATE,
allowNull: false,
},
endTime: {
type: Sequelize.DATE,
allowNull: true,
},
deleted: {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false,
},
deletedAt: {
type: Sequelize.DATE,
allowNull: true,
defaultValue: null,
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.fn('NOW'),
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.fn('NOW'),
},
});

// Every trace lookup filters by recordingId (getTraces, buildSessionPool,
// stopRecording), so that's the index that matters.
await queryInterface.addIndex('trace', ['recordingId'], {
name: 'trace_recordingId_idx',
});

await queryInterface.addIndex('trace', ['socketId'], {
name: 'trace_socketId_idx',
});
},

async down(queryInterface, Sequelize) {
await queryInterface.dropTable('trace');
},
};
50 changes: 50 additions & 0 deletions backend/db/migrations/20260506220447-extend-nav-socket-profiler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use strict';

const navElements = [
{
name: "Socket Profiler",
groupId: "Manage",
icon: "record-circle",
order: 16,
admin: true,
path: "Socket_Profiler",
component: "SocketProfiler",
}
];

/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.bulkInsert(
"nav_element",
await Promise.all(
navElements.map(async (t) => {
const groupId = await queryInterface.rawSelect(
"nav_group",
{
where: { name: t.groupId },
},
["id"]
);

t["createdAt"] = new Date();
t["updatedAt"] = new Date();
t["groupId"] = groupId;

return t;
}),
{}
)
);
},

async down(queryInterface, Sequelize) {
await queryInterface.bulkDelete(
"nav_element",
{
name: navElements.map((t) => t.name),
},
{}
);
},
};
65 changes: 65 additions & 0 deletions backend/db/models/recording.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"use strict";
Comment thread
theultimatewasteofspace marked this conversation as resolved.
const MetaModel = require("../MetaModel.js");

module.exports = (sequelize, DataTypes) => {
// Allowed recording lifecycle states. Single source of truth for the
// status validator below; add new states here rather than scattering
// string literals. "interrupted" is set on startup for recordings whose
// server died mid-capture.
const RECORDING_STATUSES = ["recording", "finished", "disconnected", "interrupted"];

class Recording extends MetaModel {
// MetaModel flags. autoTable registers this model for generic CRUD
// over sockets. fields=[] means no column whitelist (all columns
// returned). publicTable=false keeps recordings out of unauthenticated
// reads: they are admin-only artifacts, and the rows expose who was
// recorded, their session ids and their activity times.
static autoTable = true;
static fields = [];
static publicTable = false;

static associate(models) {
Recording.belongsTo(models["user"], {
foreignKey: "userId",
as: "user",
});
Recording.hasMany(models["trace"], {
foreignKey: "recordingId",
as: "traces",
});
}
}

Recording.init(
{
name: DataTypes.STRING,
status: {
type: DataTypes.STRING,
allowNull: false,
validate: { isIn: [RECORDING_STATUSES] },
},
startTime: {
type: DataTypes.DATE,
allowNull: false,
},
endTime: DataTypes.DATE,
userId: {
type: DataTypes.INTEGER,
allowNull: false,
},
participantSocketIds: DataTypes.JSONB,
excludeEvents: DataTypes.JSONB,
deleted: DataTypes.BOOLEAN,
deletedAt: DataTypes.DATE,
createdAt: DataTypes.DATE,
updatedAt: DataTypes.DATE,
},
{
sequelize,
modelName: "recording",
tableName: "recording",
}
);

return Recording;
};
61 changes: 61 additions & 0 deletions backend/db/models/trace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"use strict";
const MetaModel = require("../MetaModel.js");

module.exports = (sequelize, DataTypes) => {
class Trace extends MetaModel {
// MetaModel flags. autoTable registers this model for generic CRUD
// over sockets. fields=[] means no column whitelist. publicTable=false
// keeps traces out of the generic subscription layer, since payloads
// can hold sensitive event data — they're fetched explicitly instead.
static autoTable = true;
static fields = [];
static publicTable = false;

static associate(models) {
Trace.belongsTo(models["recording"], {
foreignKey: "recordingId",
as: "recording",
});
Trace.belongsTo(models["user"], {
foreignKey: "userId",
as: "user",
});
}
}

Trace.init(
{
recordingId: {
type: DataTypes.INTEGER,
allowNull: false,
},
userId: DataTypes.INTEGER,
socketId: DataTypes.STRING,
action: {
type: DataTypes.STRING,
allowNull: false,
},
payload: DataTypes.JSONB,
direction: {
type: DataTypes.BOOLEAN, // true = frontend -> backend, false = backend -> frontend
allowNull: false,
},
startTime: {
type: DataTypes.DATE,
allowNull: false,
},
endTime: DataTypes.DATE,
deleted: DataTypes.BOOLEAN,
deletedAt: DataTypes.DATE,
createdAt: DataTypes.DATE,
updatedAt: DataTypes.DATE,
},
{
sequelize,
modelName: "trace",
tableName: "trace",
}
);

return Trace;
};
Loading
Loading