mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-10 18:07:59 -06:00
Implemented error reporting solution
This commit is contained in:
17
lib/core/bloc/paperless_server_information_cubit.dart
Normal file
17
lib/core/bloc/paperless_server_information_cubit.dart
Normal file
@@ -0,0 +1,17 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:paperless_mobile/core/model/paperless_server_information.dart';
|
||||
import 'package:paperless_mobile/core/service/paperless_server_information_service.dart';
|
||||
|
||||
@singleton
|
||||
class PaperlessServerInformationCubit
|
||||
extends Cubit<PaperlessServerInformation> {
|
||||
final PaperlessServerInformationService service;
|
||||
|
||||
PaperlessServerInformationCubit(this.service)
|
||||
: super(PaperlessServerInformation());
|
||||
|
||||
Future<void> updateStatus() async {
|
||||
emit(await service.getInformation());
|
||||
}
|
||||
}
|
||||
9
lib/core/model/github_error_report.model.dart
Normal file
9
lib/core/model/github_error_report.model.dart
Normal file
@@ -0,0 +1,9 @@
|
||||
class GithubErrorReport {
|
||||
final String? shortDescription;
|
||||
final String? longDescription;
|
||||
|
||||
GithubErrorReport({
|
||||
this.shortDescription,
|
||||
this.longDescription,
|
||||
});
|
||||
}
|
||||
15
lib/core/model/paperless_server_information.dart
Normal file
15
lib/core/model/paperless_server_information.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
class PaperlessServerInformation {
|
||||
static const String versionHeader = 'x-version';
|
||||
static const String apiVersionHeader = 'x-api-version';
|
||||
static const String hostHeader = 'x-served-by';
|
||||
final String? version;
|
||||
final int? apiVersion;
|
||||
final String? username;
|
||||
final String? host;
|
||||
PaperlessServerInformation({
|
||||
this.host,
|
||||
this.username,
|
||||
this.version = 'unknown',
|
||||
this.apiVersion = 1,
|
||||
});
|
||||
}
|
||||
61
lib/core/service/github_issue_service.dart
Normal file
61
lib/core/service/github_issue_service.dart
Normal file
@@ -0,0 +1,61 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:paperless_mobile/core/model/github_error_report.model.dart';
|
||||
import 'package:paperless_mobile/core/widgets/error_report_page.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:paperless_mobile/extensions/dart_extensions.dart';
|
||||
|
||||
class GithubIssueService {
|
||||
static void openCreateGithubIssue({
|
||||
String? title,
|
||||
String? body,
|
||||
List<String>? labels,
|
||||
String? milestone,
|
||||
List<String>? assignees,
|
||||
String? project,
|
||||
}) {
|
||||
final Uri uri = Uri(
|
||||
scheme: "https",
|
||||
host: "github.com",
|
||||
path: "astubenbord/paperless-mobile/issues/new",
|
||||
queryParameters: {}
|
||||
..tryPutIfAbsent('title', () => title)
|
||||
//..tryPutIfAbsent('body', () => body) //TODO: Figure out how to pass long body via url
|
||||
..tryPutIfAbsent('labels', () => labels?.join(','))
|
||||
..tryPutIfAbsent('milestone', () => milestone)
|
||||
..tryPutIfAbsent('assignees', () => assignees?.join(','))
|
||||
..tryPutIfAbsent('project', () => project),
|
||||
);
|
||||
log("[GitHubIssueService] Creating GitHub issue: " + uri.toString());
|
||||
launchUrl(
|
||||
uri,
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
}
|
||||
|
||||
static void createIssueFromError(
|
||||
BuildContext context, {
|
||||
StackTrace? stackTrace,
|
||||
}) async {
|
||||
final errorDescription = await Navigator.push<GithubErrorReport>(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ErrorReportPage(
|
||||
stackTrace: stackTrace,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (errorDescription == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
return openCreateGithubIssue(
|
||||
title: errorDescription.shortDescription,
|
||||
body: errorDescription.longDescription ?? '',
|
||||
labels: ['error report'],
|
||||
);
|
||||
}
|
||||
}
|
||||
35
lib/core/service/paperless_server_information_service.dart
Normal file
35
lib/core/service/paperless_server_information_service.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:paperless_mobile/core/model/paperless_server_information.dart';
|
||||
import 'package:paperless_mobile/core/store/local_vault.dart';
|
||||
|
||||
@injectable
|
||||
class PaperlessServerInformationService {
|
||||
final BaseClient client;
|
||||
final LocalVault localStore;
|
||||
|
||||
PaperlessServerInformationService(
|
||||
@Named("timeoutClient") this.client,
|
||||
this.localStore,
|
||||
);
|
||||
|
||||
Future<PaperlessServerInformation> getInformation() async {
|
||||
final response = await client.get(Uri.parse("/api/ui_settings/"));
|
||||
final version =
|
||||
response.headers[PaperlessServerInformation.versionHeader] ?? 'unknown';
|
||||
final apiVersion = int.tryParse(
|
||||
response.headers[PaperlessServerInformation.apiVersionHeader] ?? '1');
|
||||
final String username =
|
||||
jsonDecode(utf8.decode(response.bodyBytes))['username'];
|
||||
final String? host =
|
||||
response.headers[PaperlessServerInformation.hostHeader];
|
||||
return PaperlessServerInformation(
|
||||
username: username,
|
||||
version: version,
|
||||
apiVersion: apiVersion,
|
||||
host: host,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:paperless_mobile/core/bloc/document_status_cubit.dart';
|
||||
import 'package:paperless_mobile/core/model/document_processing_status.dart';
|
||||
import 'package:paperless_mobile/di_initializer.dart';
|
||||
@@ -9,7 +10,6 @@ import 'package:paperless_mobile/features/documents/model/document.model.dart';
|
||||
import 'package:paperless_mobile/features/documents/model/paged_search_result.dart';
|
||||
import 'package:paperless_mobile/features/login/model/authentication_information.dart';
|
||||
import 'package:paperless_mobile/util.dart';
|
||||
import 'package:http_interceptor/http_interceptor.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:web_socket_channel/io.dart';
|
||||
|
||||
|
||||
164
lib/core/widgets/error_report_page.dart
Normal file
164
lib/core/widgets/error_report_page.dart
Normal file
@@ -0,0 +1,164 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/src/widgets/container.dart';
|
||||
import 'package:flutter/src/widgets/framework.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
import 'package:paperless_mobile/core/model/github_error_report.model.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
|
||||
class ErrorReportPage extends StatefulWidget {
|
||||
final StackTrace? stackTrace;
|
||||
const ErrorReportPage({super.key, this.stackTrace});
|
||||
|
||||
@override
|
||||
State<ErrorReportPage> createState() => _ErrorReportPageState();
|
||||
}
|
||||
|
||||
class _ErrorReportPageState extends State<ErrorReportPage> {
|
||||
final GlobalKey<FormBuilderState> _formKey = GlobalKey();
|
||||
|
||||
static const String shortDescriptionKey = "shortDescription";
|
||||
static const String longDescriptionKey = "longDescription";
|
||||
|
||||
bool _stackTraceCopied = false;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: true,
|
||||
appBar: AppBar(
|
||||
title: Text("Report error"),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _onSubmit,
|
||||
child: Text("Submit"),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: FormBuilder(
|
||||
key: _formKey,
|
||||
child: ListView(
|
||||
children: [
|
||||
Text(
|
||||
"""Oops, an error has occurred!
|
||||
In order to improve the app and prevent messages like these, it is greatly appreciated if you report this error with a description of what happened and the actions leading up to this window.
|
||||
Please fill the fields below and create a new issue in GitHub. Thanks!
|
||||
Note: If you have the GitHub Android app installed, the descriptions will not be taken into account! Skip these here and fill them in the GitHub issues form after submitting this report.""",
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
).padded(),
|
||||
Text(
|
||||
"Description",
|
||||
style: Theme.of(context).textTheme.subtitle1,
|
||||
).padded(),
|
||||
FormBuilderTextField(
|
||||
name: shortDescriptionKey,
|
||||
decoration: const InputDecoration(
|
||||
label: Text("Short Description"),
|
||||
hintText:
|
||||
"Please provide a brief description of what went wrong."),
|
||||
).padded(),
|
||||
FormBuilderTextField(
|
||||
name: shortDescriptionKey,
|
||||
maxLines: null,
|
||||
keyboardType: TextInputType.multiline,
|
||||
decoration: const InputDecoration(
|
||||
label: Text("Detailled Description"),
|
||||
hintText:
|
||||
"Please describe the exact actions taken that caused this error. Provide as much details as possible.",
|
||||
),
|
||||
).padded(),
|
||||
if (widget.stackTrace != null) ...[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Stack Trace",
|
||||
style: Theme.of(context).textTheme.subtitle1,
|
||||
).padded(
|
||||
const EdgeInsets.only(top: 8.0, left: 8.0, right: 8.0)),
|
||||
TextButton.icon(
|
||||
label: const Text("Copy"),
|
||||
icon: const Icon(Icons.copy),
|
||||
onPressed: _copyStackTrace,
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
"Since stack traces cannot be attached to the GitHub issue url, please copy the content of the stackTrace and paste it in the issue description. This will greatly increase the chance of quickly resolving the issue!",
|
||||
style: Theme.of(context).textTheme.caption,
|
||||
).padded(),
|
||||
Text(
|
||||
widget.stackTrace.toString(),
|
||||
style: Theme.of(context).textTheme.overline,
|
||||
).padded(),
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _copyStackTrace() {
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: '```${widget.stackTrace.toString()}```'),
|
||||
).then(
|
||||
(_) {
|
||||
setState(() => _stackTraceCopied = true);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
"Stack trace copied to clipboard.",
|
||||
),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _onSubmit() async {
|
||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||
final fk = _formKey.currentState!.value;
|
||||
if (!_stackTraceCopied) {
|
||||
final continueSubmission = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text("Continue without stack trace?"),
|
||||
content: const Text(
|
||||
"It seems you have not yet copied the stack trace. The stack trace provides valuable insights into where an error came from and how it could be fixed. Are you sure you want to continue without providing the stack trace?",
|
||||
),
|
||||
actionsAlignment: MainAxisAlignment.end,
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text("Yes, continue"),
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
),
|
||||
TextButton(
|
||||
child: const Text("No, copy stack trace"),
|
||||
onPressed: () {
|
||||
_copyStackTrace();
|
||||
Navigator.pop(context, true);
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: const Text("Cancel"),
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
),
|
||||
],
|
||||
),
|
||||
) ??
|
||||
false;
|
||||
if (!continueSubmission) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Navigator.pop(
|
||||
context,
|
||||
GithubErrorReport(
|
||||
shortDescription: fk[shortDescriptionKey],
|
||||
longDescription: fk[longDescriptionKey],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user