// 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 exportPack({ Duration penWindow = const Duration(minutes: 5), }) async { final log = BadNoteLog.instance; await log.flush(); final meta = { '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); } }