Files
BadNote/lib/diagnostics/diagnostic_export.dart

155 lines
4.7 KiB
Dart
Raw Permalink Normal View History

// Build a zip diagnostic pack the user can hand back for remote debugging.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:archive/archive.dart';
import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
import '../editor/canvas/input_diagnostics.dart';
import '../editor/input/pen_input_service.dart';
import 'badnote_log.dart';
import 'frame_sampler.dart';
import 'pen_event_ring.dart';
/// Injected at build/export time so Surface packages can be matched to git.
/// Override via `--dart-define=BADNOTE_GIT_SHA=...` in CI.
const String kBadNoteGitSha = String.fromEnvironment(
'BADNOTE_GIT_SHA',
defaultValue: 'dev',
);
const String kBadNoteBuildTime = String.fromEnvironment(
'BADNOTE_BUILD_TIME',
defaultValue: '',
);
class DiagnosticExportResult {
DiagnosticExportResult({required this.zipPath, required this.bytes});
final String zipPath;
final int bytes;
}
class DiagnosticExport {
DiagnosticExport._();
static final DiagnosticExport instance = DiagnosticExport._();
/// Flush logs and write a zip under Documents/badnote_diagnostics/.
Future<DiagnosticExportResult> exportPack({
Duration penWindow = const Duration(minutes: 5),
}) async {
final log = BadNoteLog.instance;
await log.flush();
final meta = <String, Object?>{
'exportedAt': DateTime.now().toIso8601String(),
'sessionId': log.sessionId,
'gitSha': kBadNoteGitSha,
'buildTime': kBadNoteBuildTime.isEmpty ? null : kBadNoteBuildTime,
'platform': Platform.operatingSystem,
'osVersion': Platform.operatingSystemVersion,
'localHostname': Platform.localHostname,
'numberOfProcessors': Platform.numberOfProcessors,
'flutter': {
'foundationDebug': kDebugMode,
'foundationProfile': kProfileMode,
'foundationRelease': kReleaseMode,
},
'penNative': PenInputService.instance.debugSummary,
'penActive': PenInputService.instance.isActive,
'zoom': InputDiagnostics.instance.summary(),
'frames': FrameSampler.instance.summary(),
'instructions':
'Reproduce the issue for ~3 minutes with diagnostics on, then share this zip. '
'Confirm meta.gitSha matches the CI commit you installed.',
};
final archive = Archive();
void addText(String name, String body) {
final bytes = utf8.encode(body);
archive.addFile(ArchiveFile(name, bytes.length, bytes));
}
addText('meta.json', const JsonEncoder.withIndent(' ').convert(meta));
addText(
'pen_events.json',
const JsonEncoder.withIndent(' ').convert(
PenEventRing.instance.toJsonList(window: penWindow),
),
);
addText(
'frame_samples.json',
const JsonEncoder.withIndent(' ').convert(FrameSampler.instance.toJsonList()),
);
addText(
'log_ring.json',
const JsonEncoder.withIndent(' ').convert(log.snapshotRing()),
);
// Include on-disk session NDJSON if present.
final sessionPath = log.path;
if (sessionPath != null) {
try {
final f = File(sessionPath);
if (await f.exists()) {
final bytes = await f.readAsBytes();
archive.addFile(
ArchiveFile('session.ndjson', bytes.length, bytes),
);
}
} catch (_) {}
}
// Legacy input log if it exists alongside.
try {
Directory dir;
try {
dir = await getApplicationDocumentsDirectory();
} catch (_) {
dir = await getTemporaryDirectory();
}
final legacy = File(
'${dir.path}${Platform.pathSeparator}badnote_input_log.txt',
);
if (await legacy.exists()) {
final bytes = await legacy.readAsBytes();
archive.addFile(
ArchiveFile('legacy_input_log.txt', bytes.length, bytes),
);
}
} catch (_) {}
final encoded = ZipEncoder().encode(archive);
if (encoded.isEmpty) {
throw StateError('Failed to encode diagnostic zip');
}
Directory outDir = log.directory ??
Directory(
'${(await getApplicationDocumentsDirectory()).path}'
'${Platform.pathSeparator}badnote_diagnostics',
);
if (!await outDir.exists()) {
await outDir.create(recursive: true);
}
final stamp = DateTime.now()
.toIso8601String()
.replaceAll(':', '-')
.replaceAll('.', '-');
final zipPath =
'${outDir.path}${Platform.pathSeparator}badnote_diag_$stamp.zip';
await File(zipPath).writeAsBytes(encoded, flush: true);
BadNoteLog.instance.info(
LogSubsystem.diag,
'export_pack',
fields: {'path': zipPath, 'bytes': encoded.length},
);
return DiagnosticExportResult(zipPath: zipPath, bytes: encoded.length);
}
}