Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
40 changes: 24 additions & 16 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
*.rs text eol=lf
*.toml text eol=lf
*.cs text eol=lf
*.js text eol=lf
*.ps1 text eol=lf
*.sln text eol=crlf
*.md text eol=lf
*.mustache text eol=lf
*.yaml text eol=lf

devolutions-gateway/openapi/doc/index.adoc linguist-generated merge=binary
devolutions-gateway/openapi/dotnet-client/src/** linguist-generated merge=binary
devolutions-gateway/openapi/dotnet-client/docs/** linguist-generated merge=binary
devolutions-gateway/openapi/dotnet-subscriber/src/** linguist-generated merge=binary
devolutions-gateway/openapi/ts-angular-client/api/** linguist-generated merge=binary
devolutions-gateway/openapi/ts-angular-client/model/** linguist-generated merge=binary
*.rs text eol=lf
*.toml text eol=lf
*.cs text eol=lf
*.js text eol=lf
*.ts text eol=lf
*.tsx text eol=lf
*.json text eol=lf
*.ps1 text eol=lf
*.sln text eol=crlf
*.md text eol=lf
*.mustache text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.css text eol=lf
*.scss text eol=lf
*.html text eol=lf
*.slog text eol=lf

devolutions-gateway/openapi/doc/index.adoc linguist-generated merge=binary
devolutions-gateway/openapi/dotnet-client/src/** linguist-generated merge=binary
devolutions-gateway/openapi/dotnet-client/docs/** linguist-generated merge=binary
devolutions-gateway/openapi/dotnet-subscriber/src/** linguist-generated merge=binary
devolutions-gateway/openapi/ts-angular-client/api/** linguist-generated merge=binary
devolutions-gateway/openapi/ts-angular-client/model/** linguist-generated merge=binary
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,10 @@ jobs:
run: pnpm check
working-directory: webapp

- name: Run session-recording-log tests
run: pnpm --filter @devolutions/session-recording-log test
working-directory: webapp

- name: Upload gateway-ui artifact
uses: actions/upload-artifact@v7
with:
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/publish-libraries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ jobs:
strategy:
fail-fast: false
matrix:
library: [ ts-angular-client, multi-video-player, shadow-player, web-recorder ]
library: [ ts-angular-client, multi-video-player, shadow-player, web-recorder, session-recording-log ]
include:
- library: ts-angular-client
libpath: ./devolutions-gateway/openapi/ts-angular-client
Expand All @@ -105,6 +105,8 @@ jobs:
libpath: ./webapp/packages/shadow-player
- library: web-recorder
libpath: ./webapp/packages/web-recorder
- library: session-recording-log
libpath: ./webapp/packages/session-recording-log

steps:
- name: Check out ${{ github.repository }}
Expand Down
33 changes: 17 additions & 16 deletions webapp/.editorconfig
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
# Editor configuration, see https://editorconfig.org
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

[*.ts]
quote_type = single

[*.md]
max_line_length = off
trim_trailing_whitespace = false
# Editor configuration, see https://editorconfig.org
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.ts]
quote_type = single

[*.md]
max_line_length = off
trim_trailing_whitespace = false
4 changes: 4 additions & 0 deletions webapp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ The workspace is organized into three main categories:
- Web component for real-time session streaming
- Used by recording-player for live shadowing

**@devolutions/session-recording-log** - Session recording log parser/model package
- Framework-agnostic parser, models, ordering, and in-file search for `.slog` NDJSON
- Intended for host reuse (DVLS, Gateway UI sandbox, legacy recording-player)

#### Tools (`tools/`)

**recording-player-tester** - Testing utility
Expand Down
178 changes: 178 additions & 0 deletions webapp/packages/session-recording-log/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# @devolutions/session-recording-log

Framework-agnostic models, parser, ordering, and in-file search for Session Recording Log (`.slog`) files.

## Features

- NDJSON parser that keeps valid entries even when other lines are malformed.
- Structured warning codes for parser behavior and host branching.
- Source metadata (`sourceLineNumber`, `sourceIndex`, `sourceText`) preserved for each parsed entry.
- Non-mutating display-order helper (sort by `seq`, tie-break on `sourceIndex`).
- Bounded in-memory search over visible fields with optional event/warning-linked filtering.
- Hardened bounds for large/malformed inputs (line size, string length, parameter count, depth, entry/scan caps).

## Usage

### Basic usage

```ts
import { parseSessionRecordingLog } from '@devolutions/session-recording-log';

const result = parseSessionRecordingLog(slogText);
console.log(result.completionState);
console.log(result.entries.length);
console.log(result.warnings.map((warning) => warning.code));
```

### Full runnable example

```ts
import {
getSessionRecordingLogDisplayEntries,
parseSessionRecordingLog,
searchSessionRecordingLogEntries,
} from '@devolutions/session-recording-log';

const slogText = [
'{"timestamp":"2026-07-15T21:17:49.777Z","seq":0,"event":"session.start","description":"Session started","actor":"Administrator","host":"IT-HELP-DC","sessionType":"ADConsole"}',
'{"timestamp":"2026-07-15T21:19:14.351Z","seq":1,"event":"session.action","description":"Renamed Object","object":"Help Desk Ottawa","parameters":{"Members added":"Sarah O\'Connor, Bob Smith"}}',
// Missing session.end on purpose.
].join('\n');

const parseResult = parseSessionRecordingLog(slogText);

// Parse status + warning surfacing.
console.log(parseResult.completionState); // 'ended-unexpectedly'
for (const warning of parseResult.warnings) {
console.log(`${warning.code} line=${warning.sourceLineNumber ?? '-'} seq=${warning.seq ?? '-'}`);
}

// Non-mutating display order by seq/sourceIndex.
const displayEntries = getSessionRecordingLogDisplayEntries(parseResult);
console.log(displayEntries.map((entry) => `${entry.entry.seq}:${entry.entry.event}`));

// Basic search/filter.
const actionHits = searchSessionRecordingLogEntries(parseResult.entries, "o'connor", {
eventTypes: ['session.action'],
});
const warningLinked = searchSessionRecordingLogEntries(parseResult.entries, '', {
onlyWithWarnings: true,
warnings: parseResult.warnings,
});

console.log(actionHits.length); // 1
console.log(warningLinked.length); // entries linked to parser warnings
```

## API Reference

Public API:

- `parseSessionRecordingLog(text, options?)`
- `getSessionRecordingLogDisplayEntries(parseResult)`
- `searchSessionRecordingLogEntries(entries, query, options?)`
- `isSessionRecordingLogFileName(fileName)`

Host contract:

- Hosts detect `.slog` artifacts from `recording.json` file names (for example `recording-0.slog`).
- Hosts keep original bytes for download scenarios.
- Hosts decode UTF-8 text and pass the decoded NDJSON text to the parser.
- This package does not reconstruct downloadable `.slog` content.

Architecture Decision Record 2 (ADR-2) canonical contract:

- `parseSessionRecordingLog` returns:
- `entries: ParsedSessionRecordingLogEntry[]` in original file order.
- `warnings: SessionRecordingLogWarning[]` with structured `code`.
- `completionState: 'complete' | 'ended-unexpectedly'`.
- `ParsedSessionRecordingLogEntry` exposes `entry`, `sourceLineNumber`, and `sourceIndex`.
- Public warning codes follow ADR-2 categories:
- `malformed-line`
- `missing-session-start`
- `missing-session-end`
- `duplicate-sequence`
- `missing-sequence`
- `sequence-order-mismatch`
- `unknown-event-type`
- `invalid-field`
- `entry-limit-exceeded`
- `string-truncated`
- Additive warning:
- `unterminated-final-line` (informational; valid final line without newline is not semantically invalid).
- Intended for Architecture Decision Record (ADR) addendum documentation rather than replacement of canonical malformed handling.

Schema-limit options:

- `maxLineLengthBytes`
- `maxRetainedSourceTextBytes`
- `maxStringLength`
- `maxParameterCount`
- `maxObjectDepth`
- `maxParsedEntries`
- `maxScannedLines`
- `maxMissingSequenceWarnings`
- `maxUnknownFieldCount`
- `maxWarnings`

Ratified v1 defaults (review decision):

- `maxLineLengthBytes`: `262144` (256 KiB)
- `maxRetainedSourceTextBytes`: `8388608` (8 MiB)
- `maxStringLength`: `4096`
- `maxParameterCount`: `200`
- `maxObjectDepth`: `8`
- `maxParsedEntries`: `10000`
- `maxScannedLines`: `20000`
- `maxMissingSequenceWarnings`: `1000`
- `maxUnknownFieldCount`: `100`
- `maxWarnings`: `10000`
- search default limit: `100`
- search max limit: `1000`
Comment thread
kristahouse marked this conversation as resolved.

## Project Structure

```txt
session-recording-log/
src/
fixtures/ # sample .slog fixtures used by package tests
parser.ts
ordering.ts
search.ts
manifest.ts
model.ts
index.ts
parser.test.ts
helpers.test.ts
README.md
package.json
package.dist.json
tsconfig.json
vite.config.ts
```

## Dependencies

- Runtime dependencies: none
- Dev dependencies: TypeScript, Vite, Vitest, Biome, `vite-plugin-dts`, `vite-plugin-static-copy`

## For Developers

From `webapp/`:

```bash
pnpm --filter @devolutions/session-recording-log check:write
pnpm --filter @devolutions/session-recording-log check
pnpm --filter @devolutions/session-recording-log test
pnpm --filter @devolutions/session-recording-log build
```

## Review-driven hardening updates (2026-07-17)

- Added bounded missing-sequence emission to prevent unbounded gap loops on sparse large `seq` ranges.
- Replaced recursive depth traversal with iterative depth checks to prevent stack-overflow failure on deeply nested JSON.
- Added malformed-heavy input bounding with `maxScannedLines`, and finite default `maxParsedEntries`.
- Fixed lifecycle classification so only empty/whitespace-only input is `complete`; non-empty input without `session.end` remains `ended-unexpectedly`.
- Added top-level unknown field cap (`maxUnknownFieldCount`) with `entry-limit-exceeded` warning when truncated.
- Split unknown future events into explicit `unknown-event-type` warning code.
- Clarified alias intent: `SessionRecordingLogRecord` is retained for AD historical naming compatibility.
19 changes: 19 additions & 0 deletions webapp/packages/session-recording-log/build.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env pwsh

$ErrorActionPreference = "Stop"

Push-Location -Path $PSScriptRoot

try
{
pnpm install

pnpm --filter @devolutions/session-recording-log... build

Set-Location -Path ./dist/
npm pack
}
finally
{
Pop-Location
}
30 changes: 30 additions & 0 deletions webapp/packages/session-recording-log/package.dist.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "@devolutions/session-recording-log",
"version": "0.1.0",
"description": "Framework-agnostic parser and models for Devolutions Gateway Session Recording Log NDJSON files",
"type": "module",
"main": "./index.js",
"module": "./index.js",
"types": "./index.d.ts",
"exports": {
".": {
"import": "./index.js",
"types": "./index.d.ts"
}
},
"repository": {
"type": "git",
"url": "https://github.com/Devolutions/devolutions-gateway.git"
},
"keywords": [
"recording",
"session-recording",
"ndjson",
"slog",
"parser"
],
"license": "MIT OR Apache-2.0",
"publishConfig": {
"access": "public"
}
}
28 changes: 28 additions & 0 deletions webapp/packages/session-recording-log/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@devolutions/session-recording-log",
"private": true,
"version": "0.0.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"scripts": {
"build": "tsc && vite build",
"check": "biome check ./src vite.config.ts",
"check:write": "biome check --write ./src vite.config.ts",
"test": "vitest run"
Comment thread
kristahouse marked this conversation as resolved.
},
"devDependencies": {
"@types/node": "^20.19.0",
"typescript": "~5.6.2",
"vite": "^6.2.1",
"vite-plugin-dts": "^4.3.0",
"vite-plugin-static-copy": "^2.3.0",
Comment thread
kristahouse marked this conversation as resolved.
"vitest": "^3.1.1"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{"timestamp":"2026-07-15T16:15:35.140Z","seq":0,"actor":"Administrator","locale":"en-US","host":"IT-HELP-DC","sessionType":"ADConsole","event":"session.start","description":"Session started"}
{"timestamp":"2026-07-15T16:15:41.054Z","seq":1,"event":"session.action","description":"Created User","object":"Help Desk Ottawa","parameters":{"New name":"Help Desk Ottawa/Hull"}}
{"timestamp":"2026-07-15T16:16:32.759Z","seq":2,"event":"session.end","description":"Session ended"}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{"timestamp":"2026-07-15T21:17:49.777Z","seq":0,"actor":"Administrator","host":"IT-HELP-DC","sessionType":"ADConsole","event":"session.start","description":"Session started"}
{"timestamp":"2026-07-15T21:19:14.351Z","seq":1,"event":"session.action","description":"Renamed Object","object":"Help Desk Ottawa","parameters":{"New name":"Help Desk Ottawa/Hull","Members added":"Sarah O'Connor, Bob Smith"}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{"timestamp":"2026-07-15T21:17:49.777Z","seq":0,"event":"session.start","description":"Session started"}
{"timestamp":"2026-07-15T21:19:14.351Z","seq":1,"event":"session.future","description":"Future action","parameters":{"Members added":"Sarah O'Connor, Bob Smith"}}
{"timestamp":"2026-07-15T21:20:00.000Z","seq":2,"event":"session.end","description":"Session ended"}
Loading
Loading