Protocol 15: one shared session log on the server, chat never logged, Mark a problem

Every app streams its redacted diagnostic lines (server time, batched
every 10 s, gzip, buffered while offline) to POST /l/; the server merges
them with its own into session-YYYY-MM-DD.log in time order, tagged by
who. Opt-out in Settings > About; Mark a problem (Ctrl+Shift+B, the
player menus, About) writes a marker with an optional note. Copy
diagnostics can include the server's session log. Chat text never
reaches any log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-12 19:41:06 +03:00
1 parent 4ab37c55cf
commit 7487b4deb1
29 files changed
+1380 -47

No files matched your search

+87
View File
@@ -0,0 +1,87 @@
import 'dart:convert';
import 'dart:io';
import 'package:logging/logging.dart';
import 'package:sync_relay_core/core.dart';
import 'package:test/test.dart';
void main() {
test('a batch survives gzip both ways; a bomb or junk is refused', () {
const b = DiagBatch(
version: 'WT 1.0',
lines: [DiagLine(t: 5, level: 'WARNING', logger: 'mpv', message: 'x\n error: y')],
dropped: 2,
marks: [DiagMark(t: 7, note: 'lag')]);
final back = DiagBatch.decode(b.encode());
expect(back.toJson(), b.toJson());
expect(DiagBatch.decode(const DiagBatch(share: false).encode()).share, isFalse);
expect(() => DiagBatch.decode(gzip.encode(List.filled(2 << 20, 32)), maxBytes: 1 << 20), throwsFormatException);
expect(() => DiagBatch.decode(utf8.encode('plain')), throwsFormatException);
});
group('DiagUploader', () {
late HttpServer http;
final got = <DiagBatch>[];
var accept = true;
setUp(() async {
got.clear();
accept = true;
http = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
http.listen((req) async {
final body = await req.fold<List<int>>([], (a, c) => a..addAll(c));
if (accept) got.add(DiagBatch.decode(body));
req.response.statusCode = accept ? 204 : 503;
await req.response.close();
});
});
tearDown(() => http.close(force: true));
DiagUploader make({bool connected = true, int buffer = 1 << 20}) => DiagUploader(
target: () => connected ? (base: Uri.parse('http://127.0.0.1:${http.port}'), token: 't', offsetMs: 1000) : null,
version: 'WT 1.0',
bufferBytes: buffer);
test('sends its records on the server clock, not the hosted server\'s; keeps them while it cannot', () async {
final u = make()..start();
Logger('relay').info('hello api_key=abc');
Logger('server.relay').info('the server logs this itself');
await Future<void>.delayed(Duration.zero);
accept = false;
expect(await u.flush(), isFalse);
expect(u.pending, 1);
accept = true;
expect(await u.flush(), isTrue);
final line = got.single.lines.single;
expect(line.message, 'hello api_key=***');
expect(line.logger, 'relay');
expect(got.single.version, 'WT 1.0');
await u.stop();
});
test('a full buffer drops the oldest and says how many', () async {
final u = make(buffer: 400);
for (var i = 0; i < 10; i++) {
u.add(DiagLine(t: i, level: 'INFO', logger: 'a', message: 'line $i ${'x' * 40}'));
}
expect(await u.flush(), isTrue);
expect(got.single.dropped, greaterThan(0));
expect(got.single.lines.last.message, startsWith('line 9'));
expect(got.single.lines.first.t, greaterThanOrEqualTo(1000), reason: 'offset to the server clock');
});
test('opted out: no lines, one "does not share" per connection; marks still go', () async {
final u = make()
..share = false
..start();
Logger('relay').info('private');
expect(await u.flush(), isTrue);
expect(await u.flush(), isTrue);
expect(got.length, 1);
expect(got.single.share, isFalse);
expect(got.single.lines, isEmpty);
expect(await u.markProblem('it\nfroze'), isTrue);
expect(got.last.marks.single.note, 'it froze');
await u.stop();
});
});
}
+12
View File
@@ -19,6 +19,7 @@ void main() {
expect(redactLog('open http://1.2.3.4:8080/r/default/$tok/2/0/name'), 'open http://1.2.3.4:8080/r/default/***/2/0/name');
expect(redactLog('ws://h:1/r/default/$tok/source?q=7'), 'ws://h:1/r/default/***/source?q=…');
expect(redactLog('/m/default/$tok/file.mkv range=bytes=0-'), '/m/default/***/file.mkv range=bytes=0-');
expect(redactLog('POST /l/default/$tok'), 'POST /l/default/***');
expect(redactLog('edl://!no_clip;%160%http://h/r/default/$tok/2/0/n;!new_stream;!no_clip;%160%http://h/r/default/$tok/2/1/n'),
isNot(contains(tok)));
});
@@ -85,5 +86,16 @@ void main() {
expect(small.files().map((x) => x.uri.pathSegments.last), ['app-2026-10-12.1.log', 'app-2026-10-13.log']);
small.close();
});
test('the session log shares the total budget, never touching another writer\'s today', () {
final now = DateTime(2026, 10, 12, 10);
File('${dir.path}/session-2026-10-10.log').writeAsStringSync('s' * 100);
File('${dir.path}/session-2026-10-12.log').writeAsStringSync('s' * 100);
File('${dir.path}/app-2026-10-11.log').writeAsStringSync('a' * 100);
final f = LogFiles(dir.path, maxTotalBytes: 150, shareWith: const ['session'], now: () => now)..open();
final left = dir.listSync().map((e) => e.uri.pathSegments.last).toSet();
expect(left, {'session-2026-10-12.log', 'app-2026-10-12.log'});
f.close();
});
});
}