Refine school script diagnostics and document-state capture
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
const appVersion = '0.7.1';
|
||||
const appBuildNumber = 19;
|
||||
const appVersion = '0.7.2';
|
||||
const appBuildNumber = 20;
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:webview_flutter/webview_flutter.dart';
|
||||
import 'package:webview_flutter_android/webview_flutter_android.dart';
|
||||
|
||||
import '../../services/diagnostic_log.dart';
|
||||
import 'school_browser_diagnostics.dart';
|
||||
|
||||
class SchoolBrowserInfo {
|
||||
const SchoolBrowserInfo({this.chromiumVersion});
|
||||
@@ -27,18 +28,9 @@ Future<SchoolBrowserInfo> configureSchoolBrowser(
|
||||
await cookies.setAcceptThirdPartyCookies(android, true);
|
||||
}
|
||||
await controller.setOnConsoleMessage((message) {
|
||||
final text = message.message.toLowerCase();
|
||||
final code = text.contains('mixed content')
|
||||
? 'mixed_content'
|
||||
: text.contains('content security policy')
|
||||
? 'content_security_policy'
|
||||
: text.contains('cors')
|
||||
? 'cross_origin'
|
||||
: text.contains('refused to')
|
||||
? 'resource_refused'
|
||||
: message.level == JavaScriptLogLevel.error
|
||||
? 'script_error'
|
||||
: null;
|
||||
final code =
|
||||
schoolConsoleCode(message.message) ??
|
||||
(message.level == JavaScriptLogLevel.error ? 'script_error' : null);
|
||||
if (code != null) {
|
||||
DiagnosticLog.instance.add(
|
||||
'school_console_error',
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
import '../../services/diagnostic_log.dart';
|
||||
|
||||
/// Convert errors into fixed labels; never export the original console text.
|
||||
String? schoolConsoleCode(String message) {
|
||||
final text = message.toLowerCase();
|
||||
if (text.contains('mixed content')) return 'mixed_content';
|
||||
if (text.contains('content security policy')) {
|
||||
return 'content_security_policy';
|
||||
}
|
||||
if (text.contains('cors')) return 'cross_origin';
|
||||
if (text.contains('refused to')) return 'resource_refused';
|
||||
if (text.contains('referenceerror')) {
|
||||
if (RegExp(r'(?:jquery|\$) is not defined').hasMatch(text)) {
|
||||
return 'jquery_missing';
|
||||
}
|
||||
if (text.contains('beangle is not defined')) return 'beangle_missing';
|
||||
if (text.contains('require is not defined')) return 'require_missing';
|
||||
return 'reference_error';
|
||||
}
|
||||
if (text.contains('syntaxerror')) return 'syntax_error';
|
||||
if (text.contains('typeerror')) return 'type_error';
|
||||
if (text.contains('securityerror')) return 'security_error';
|
||||
if (text.contains('evalerror')) return 'eval_error';
|
||||
if (text.contains('rangeerror')) return 'range_error';
|
||||
return null;
|
||||
}
|
||||
|
||||
// Counts and known library booleans only. This never reads text, forms, cookies,
|
||||
// storage, or resource URLs. A different document or login form returns null.
|
||||
const schoolPageDiagnosticsScript = r'''(() => {
|
||||
if(location.href!==__EXPECTED_URL__ || document.querySelector('input[type="password"]'))return 'null';
|
||||
const links=Array.from(document.querySelectorAll('link[rel~="stylesheet"]'));
|
||||
const resources=performance.getEntriesByType('resource');
|
||||
return JSON.stringify({scripts:document.scripts.length,styles:document.styleSheets.length,
|
||||
links:links.length,loaded:links.filter(link=>link.sheet!==null).length,
|
||||
jquery:typeof window.jQuery==='function',beangle:window.beangle!==null && typeof window.beangle==='object',
|
||||
require:typeof window.require==='function',
|
||||
script202:resources.filter(r=>r.initiatorType==='script' && r.responseStatus===202).length,
|
||||
style202:resources.filter(r=>r.initiatorType==='link' && r.responseStatus===202).length});
|
||||
})()''';
|
||||
|
||||
void recordSchoolPageState(DiagnosticLog log, String school, Object raw) {
|
||||
dynamic value = raw;
|
||||
try {
|
||||
for (var i = 0; i < 2 && value is String; i++) {
|
||||
value = jsonDecode(value);
|
||||
}
|
||||
if (value is! Map) return;
|
||||
const counts = [
|
||||
'scripts',
|
||||
'styles',
|
||||
'links',
|
||||
'loaded',
|
||||
'script202',
|
||||
'style202',
|
||||
];
|
||||
const libraries = ['jquery', 'beangle', 'require'];
|
||||
if (counts.any(
|
||||
(key) => value[key] is! int || value[key] < 0 || value[key] > 100000,
|
||||
) ||
|
||||
libraries.any((key) => value[key] is! bool)) {
|
||||
return;
|
||||
}
|
||||
for (final key in counts) {
|
||||
if ((key == 'script202' || key == 'style202') && value[key] == 0) {
|
||||
continue;
|
||||
}
|
||||
log.add(
|
||||
'school_asset_state',
|
||||
fields: {
|
||||
'school': school,
|
||||
'code': key == 'script202'
|
||||
? 'script_202'
|
||||
: key == 'style202'
|
||||
? 'stylesheet_202'
|
||||
: key,
|
||||
'count': value[key] as int,
|
||||
},
|
||||
);
|
||||
}
|
||||
if (school == 'undergraduate') {
|
||||
for (final key in libraries) {
|
||||
log.add(
|
||||
'school_library_state',
|
||||
fields: {'school': school, 'code': key, 'result': value[key] as bool},
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
/* Diagnostics must not interfere with the school page. */
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> recordSchoolPageDiagnostics(
|
||||
WebViewController web,
|
||||
String school,
|
||||
String url, {
|
||||
bool Function()? isCurrent,
|
||||
}) async {
|
||||
final uri = Uri.tryParse(url);
|
||||
if (uri == null ||
|
||||
uri.scheme != 'https' ||
|
||||
!const {'eams.uestc.edu.cn', 'yjsjy.uestc.edu.cn'}.contains(uri.host)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (isCurrent?.call() == false) return;
|
||||
if (await web.currentUrl() != url || isCurrent?.call() == false) return;
|
||||
final raw = await web.runJavaScriptReturningResult(
|
||||
schoolPageDiagnosticsScript.replaceAll(
|
||||
'__EXPECTED_URL__',
|
||||
jsonEncode(url),
|
||||
),
|
||||
);
|
||||
if (await web.currentUrl() != url || isCurrent?.call() == false) {
|
||||
return;
|
||||
}
|
||||
recordSchoolPageState(DiagnosticLog.instance, school, raw);
|
||||
} catch (_) {
|
||||
/* No original exceptions or page data enter the log. */
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import 'school_exam_adapter.dart';
|
||||
import 'school_login_flow.dart';
|
||||
import 'school_page_probe.dart';
|
||||
import 'school_browser.dart';
|
||||
import 'school_browser_diagnostics.dart';
|
||||
|
||||
enum SchoolConnectionState {
|
||||
signedOut,
|
||||
@@ -34,7 +35,9 @@ class SchoolSession extends ChangeNotifier {
|
||||
controller,
|
||||
'graduate',
|
||||
));
|
||||
if (!hasOpened || generation != _pageGeneration || serial != _openSerial) {
|
||||
if (!hasOpened ||
|
||||
generation != _pageGeneration ||
|
||||
serial != _openSerial) {
|
||||
return;
|
||||
}
|
||||
browserVersion = info.chromiumVersion;
|
||||
@@ -106,7 +109,19 @@ class SchoolSession extends ChangeNotifier {
|
||||
_loading = true;
|
||||
notifyListeners();
|
||||
},
|
||||
onPageFinished: _loaded,
|
||||
onPageFinished: (url) {
|
||||
final generation = _pageGeneration;
|
||||
unawaited(
|
||||
recordSchoolPageDiagnostics(
|
||||
web,
|
||||
'graduate',
|
||||
url,
|
||||
isCurrent: () =>
|
||||
hasOpened && !_routeClosed && generation == _pageGeneration,
|
||||
),
|
||||
);
|
||||
unawaited(_loaded(url));
|
||||
},
|
||||
onWebResourceError: (error) async {
|
||||
DiagnosticLog.instance.resourceFailure(
|
||||
'graduate',
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'page_probe.dart';
|
||||
import 'timetable_adapter.dart';
|
||||
import 'exam_adapter.dart';
|
||||
import '../school_browser.dart';
|
||||
import '../school_browser_diagnostics.dart';
|
||||
|
||||
class UndergraduateSession extends ChangeNotifier {
|
||||
UndergraduateSession({this.configureBrowser = configureSchoolBrowser});
|
||||
@@ -138,7 +139,18 @@ class UndergraduateSession extends ChangeNotifier {
|
||||
_emit();
|
||||
}
|
||||
},
|
||||
onPageFinished: _loaded,
|
||||
onPageFinished: (url) {
|
||||
final generation = _generation, document = _document;
|
||||
unawaited(
|
||||
recordSchoolPageDiagnostics(
|
||||
web,
|
||||
'undergraduate',
|
||||
url,
|
||||
isCurrent: () => _valid(generation) && document == _document,
|
||||
),
|
||||
);
|
||||
unawaited(_loaded(url));
|
||||
},
|
||||
onWebResourceError: (error) async {
|
||||
DiagnosticLog.instance.resourceFailure(
|
||||
'undergraduate',
|
||||
|
||||
@@ -3,6 +3,8 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import '../app/app_version.dart';
|
||||
|
||||
/// Small, structured breadcrumbs. Never accepts raw URLs, HTML or exceptions.
|
||||
class DiagnosticLog {
|
||||
DiagnosticLog({DateTime Function()? clock}) : clock = clock ?? DateTime.now;
|
||||
@@ -183,7 +185,9 @@ class DiagnosticLog {
|
||||
_trim();
|
||||
await flush();
|
||||
final header =
|
||||
'TPSchedule diagnostic log\nWindow: last 10 minutes (UTC timestamps)\n'
|
||||
'TPSchedule diagnostic log\nApp: TPSchedule $appVersion+$appBuildNumber\n'
|
||||
'Exported at: ${clock().toUtc().toIso8601String()}\n'
|
||||
'Window: last 10 minutes (UTC timestamps)\n'
|
||||
'Up to 500 recent events; no passwords, cookies, HTML or feedback text.\n';
|
||||
return Uint8List.fromList(
|
||||
utf8.encode('$header${_entries.map(jsonEncode).join('\n')}\n'),
|
||||
|
||||
Reference in New Issue
Block a user