Commit 14c6b20f authored by Wilko Manger's avatar Wilko Manger

Make reporting errors to Sentry optional

parent dbffd421
......@@ -17,11 +17,9 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:pattle/src/app.dart';
import 'package:pattle/src/sentry.dart' as sentry;
import 'package:flutter/widgets.dart';
Future<void> main() async {
await sentry.init();
sentry.wrap(() => runApp(App()));
}
import 'src/app.dart';
import 'src/app_bloc.dart';
Future<void> main() async => (await AppBloc.create()).wrap(() => runApp(App()));
// Copyright (C) 2019 wilko
//
// This file is part of Pattle.
//
// Pattle is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Pattle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Pattle. If not, see <https://www.gnu.org/licenses/>.
import 'dart:async';
import 'package:meta/meta.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/sentry.dart';
import 'package:respect_24_hour/respect_24_hour.dart';
import 'package:rxdart/rxdart.dart';
import 'package:pattle/src/di.dart' as di;
import 'storage.dart';
class AppBloc {
Sentry sentry;
final Storage storage;
final String _mayReportCrashesStorageKey = 'may_report_crashes';
static AppBloc _instance;
AppBloc._({@required this.storage});
factory AppBloc() => _instance;
static Future<AppBloc> create() async {
await DotEnv().load();
final use24Hour = await Respect24Hour.get24HourFormat;
di.registerUse24HourFormat(use24Hour);
_instance = AppBloc._(
storage: await Storage.open(),
);
return _instance;
}
final _loggedInSubj = BehaviorSubject<bool>();
Observable<bool> get loggedIn => _loggedInSubj.stream;
Future<void> checkIfLoggedIn() async {
di.registerStore();
final localUser = await LocalUser.fromStore(di.getStore());
if (localUser != null) {
di.registerLocalUser(localUser);
}
final loggedIn = localUser != null;
_loggedInSubj.add(loggedIn);
if (loggedIn && await getMayReportCrashes()) {
sentry = await Sentry.create();
}
}
Future<void> notifyLogin() async {
if (await getMayReportCrashes() && sentry == null) {
sentry = await Sentry.create();
}
}
void wrap(Function run) => runZoned<Future<void>>(
() async => run(),
onError: (error, stackTrace) => sentry?.reportError(error, stackTrace),
);
String get build => DotEnv().env['BUILD_TYPE'];
bool _mayReportCrashes;
FutureOr<bool> getMayReportCrashes({bool lookInStorage = true}) {
if (_mayReportCrashes == null && !lookInStorage) {
setMayReportCrashes(build != 'fdroid');
return _mayReportCrashes;
} else if (_mayReportCrashes != null) {
return _mayReportCrashes;
} else {
return storage[_mayReportCrashesStorageKey]
.then((v) => v as bool)
.then((v) => _mayReportCrashes = v);
}
}
void setMayReportCrashes(value) {
_mayReportCrashes = value;
storage[_mayReportCrashesStorageKey] = value;
}
}
......@@ -41,7 +41,7 @@ Future<void> registerHomeserverWith(Url url) async {
Store getStore() => inj.getDependency<Store>();
void registerStore() {
final store = SqfliteStore(path: 'pattle.sqlite');
final store = SqfliteStore(path: 'matrix.sqlite');
inj.registerSingleton<Store>((_) => store, override: true);
}
......
......@@ -27,153 +27,150 @@ import 'package:package_info/package_info.dart';
import 'package:device_info/device_info.dart';
import 'dart:io' show Platform;
SentryClient _sentry;
Future<void> _reportError(dynamic error, dynamic stackTrace) async {
print('Caught error: $error');
if (_isInDebugMode) {
if (error is Response) {
print('statusCode: ${error.statusCode}');
print('headers: ${error.headers}');
print('body: ${error.body}');
} else if (error is matrix.MatrixException) {
print('body: ${error.body}');
}
class Sentry {
SentryClient _client;
if (stackTrace != null) {
print(stackTrace);
}
} else {
if (error is Response) {
var body;
try {
body = json.decode(error.body);
} on FormatException {
body = error.body?.toString();
Sentry();
static Future<Sentry> create() async {
final sentry = Sentry();
final client = SentryClient(
dsn: DotEnv().env['SENTRY_DSN'],
environmentAttributes: await sentry._environment,
);
FlutterError.onError = (FlutterErrorDetails details) {
if (sentry._isInDebugMode) {
FlutterError.dumpErrorToConsole(details);
} else {
// Report to zone
Zone.current.handleUncaughtError(details.exception, details.stack);
}
};
await _sentry.capture(
event: Event(
exception: error,
stackTrace: stackTrace,
extra: {
'status_code': error.statusCode,
'headers': error.headers,
'body': body,
},
),
);
} else if (error is matrix.MatrixException) {
await _sentry.capture(
event: Event(
sentry._client = client;
return sentry;
}
Future<void> reportError(dynamic error, dynamic stackTrace) async {
print('Caught error: $error');
if (_isInDebugMode) {
if (error is Response) {
print('statusCode: ${error.statusCode}');
print('headers: ${error.headers}');
print('body: ${error.body}');
} else if (error is matrix.MatrixException) {
print('body: ${error.body}');
}
if (stackTrace != null) {
print(stackTrace);
}
} else {
if (error is Response) {
var body;
try {
body = json.decode(error.body);
} on FormatException {
body = error.body?.toString();
}
await _client.capture(
event: Event(
exception: error,
stackTrace: stackTrace,
extra: {
'status_code': error.statusCode,
'headers': error.headers,
'body': body,
},
),
);
} else if (error is matrix.MatrixException) {
await _client.capture(
event: Event(
exception: error,
stackTrace: stackTrace,
extra: {
'body': error.body,
},
),
);
} else {
await _client.captureException(
exception: error,
stackTrace: stackTrace,
extra: {
'body': error.body,
},
),
);
} else {
await _sentry.captureException(
exception: error,
stackTrace: stackTrace,
);
);
}
}
}
}
bool get _isInDebugMode {
bool inDebugMode = false;
bool get _isInDebugMode {
bool inDebugMode = false;
// Set to true if running debug mode (where asserts are evaluated)
assert(inDebugMode = true);
// Set to true if running debug mode (where asserts are evaluated)
assert(inDebugMode = true);
return inDebugMode;
}
Future<Event> get _environment async {
final deviceInfo = DeviceInfoPlugin();
User user;
Os os;
Device device;
if (Platform.isAndroid) {
final info = await deviceInfo.androidInfo;
user = User(id: info.androidId);
return inDebugMode;
}
os = Os(
name: 'Android',
version: info.version.release,
build: info.version.sdkInt.toString(),
);
Future<Event> get _environment async {
final deviceInfo = DeviceInfoPlugin();
device = Device(
model: info.model,
manufacturer: info.manufacturer,
brand: info.brand,
simulator: !info.isPhysicalDevice,
);
} else if (Platform.isIOS) {
final info = await deviceInfo.iosInfo;
User user;
Os os;
Device device;
user = User(id: info.identifierForVendor);
if (Platform.isAndroid) {
final info = await deviceInfo.androidInfo;
os = Os(
name: 'iOS',
version: info.systemVersion,
);
user = User(id: info.androidId);
device = Device(
family: info.model,
model: info.utsname.machine,
simulator: !info.isPhysicalDevice,
);
}
os = Os(
name: 'Android',
version: info.version.release,
build: info.version.sdkInt.toString(),
);
final packageInfo = await PackageInfo.fromPlatform();
return Event(
release: packageInfo.version,
userContext: user,
environment: 'production',
contexts: Contexts(
device: device,
os: os,
app: App(
build: packageInfo.buildNumber,
buildType: DotEnv().env['BUILD_TYPE'],
),
),
);
}
device = Device(
model: info.model,
manufacturer: info.manufacturer,
brand: info.brand,
simulator: !info.isPhysicalDevice,
);
} else if (Platform.isIOS) {
final info = await deviceInfo.iosInfo;
Future<void> init() async {
await DotEnv().load();
user = User(id: info.identifierForVendor);
_sentry = SentryClient(
dsn: DotEnv().env['SENTRY_DSN'],
environmentAttributes: await _environment,
);
os = Os(
name: 'iOS',
version: info.systemVersion,
);
FlutterError.onError = (FlutterErrorDetails details) {
if (_isInDebugMode) {
FlutterError.dumpErrorToConsole(details);
} else {
// Report to zone
Zone.current.handleUncaughtError(details.exception, details.stack);
device = Device(
family: info.model,
model: info.utsname.machine,
simulator: !info.isPhysicalDevice,
);
}
};
}
void wrap(Function run) {
runZoned<Future<void>>(
() async {
run();
},
onError: (error, stackTrace) {
_reportError(error, stackTrace);
},
);
final packageInfo = await PackageInfo.fromPlatform();
return Event(
release: packageInfo.version,
userContext: user,
environment: 'production',
contexts: Contexts(
device: device,
os: os,
app: App(
build: packageInfo.buildNumber,
buildType: DotEnv().env['BUILD_TYPE'],
),
),
);
}
}
// Copyright (C) 2019 wilko
//
// This file is part of Pattle.
//
// Pattle is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Pattle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Pattle. If not, see <https://www.gnu.org/licenses/>.
import 'package:sqflite/sqflite.dart';
class Storage {
static const _preferencesTable = 'preferences';
static const _keyColumn = 'key';
static const _valueColumn = 'value';
final Database db;
Storage(this.db);
static Future<Storage> open() async {
final db = await openDatabase(
'pattle.sqlite',
version: 1,
onCreate: (db, version) async {
await db.execute(
'''
CREATE TABLE IF NOT EXISTS $_preferencesTable (
$_keyColumn TEXT PRIMARY KEY,
$_valueColumn TEXT
);
''',
);
},
);
return Storage(db);
}
Future<dynamic> operator [](String key) async {
final results = await db.query(
_preferencesTable,
where: '$_keyColumn = ?',
whereArgs: [key],
);
if (results.isNotEmpty) {
return results.first[_valueColumn];
} else {
return null;
}
}
void operator []=(String key, dynamic value) {
db.insert(
_preferencesTable,
{_keyColumn: key, _valueColumn: value},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
}
// Copyright (C) 2019 Wilko Manger
// Copyright (C) 2019 wilko
//
// This file is part of Pattle.
//
......@@ -15,31 +15,6 @@
// You should have received a copy of the GNU Affero General Public License
// along with Pattle. If not, see <https://www.gnu.org/licenses/>.
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:rxdart/rxdart.dart';
import 'package:pattle/src/di.dart' as di;
import 'package:respect_24_hour/respect_24_hour.dart';
final bloc = InitialBloc();
class InitialBloc {
final _loggedInSubj = BehaviorSubject<bool>();
Observable<bool> get loggedIn => _loggedInSubj.stream;
// General app initialization
void init() async {
var use24Hour = await Respect24Hour.get24HourFormat;
di.registerUse24HourFormat(use24Hour);
}
void checkIfLoggedIn() async {
di.registerStore();
var localUser = await LocalUser.fromStore(di.getStore());
if (localUser != null) {
di.registerLocalUser(localUser);
}
_loggedInSubj.add(localUser != null);
}
abstract class Bloc {
void dispose() {}
}
......@@ -18,17 +18,17 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:pattle/src/app.dart';
import 'package:pattle/src/ui/initial/initial_bloc.dart';
import '../../app_bloc.dart';
class InitialPageState extends State<InitialPage> {
final AppBloc bloc = AppBloc();
StreamSubscription<bool> subscription;
@override
void initState() {
super.initState();
bloc.init();
subscription = bloc.loggedIn.listen((loggedIn) {
var route;
......
......@@ -25,6 +25,8 @@ class AdvancedPage extends StatefulWidget {
}
class AdvancedPageState extends State<AdvancedPage> {
final StartBloc bloc = StartBloc();
final homeserverTextController = TextEditingController();
@override
......
......@@ -31,6 +31,8 @@ class UsernamePage extends StatefulWidget {
}
class UsernamePageState extends State<UsernamePage> {
final StartBloc bloc = StartBloc();
final usernameController = TextEditingController();
StreamSubscription subscription;
......@@ -59,7 +61,6 @@ class UsernamePageState extends State<UsernamePage> {
@override
void dispose() {
super.dispose();
subscription.cancel();
}
......
......@@ -20,10 +20,14 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/app.dart';
import 'package:pattle/src/app_bloc.dart';
import 'package:pattle/src/ui/resources/localizations.dart';
import 'package:pattle/src/ui/start/start_bloc.dart';
import 'package:pattle/src/ui/util/future_or_builder.dart';
class PasswordPageState extends State<PasswordPage> {
final StartBloc bloc = StartBloc();
StreamSubscription subscription;
@override
......@@ -34,6 +38,7 @@ class PasswordPageState extends State<PasswordPage> {
subscription = bloc.loginStream.listen((state) {
if (state == RequestState.success) {
bloc.dispose();
Navigator.pushNamedAndRemoveUntil(
context,
Routes.chats,
......@@ -111,6 +116,32 @@ class PasswordPageState extends State<PasswordPage> {
},
),
SizedBox(height: 16),
Row(
children: <Widget>[
FutureOrBuilder<bool>(
futureOr: AppBloc().getMayReportCrashes(lookInStorage: false),
builder: (
BuildContext context,
AsyncSnapshot<bool> snapshot,
) {
final mayReportCrashes = snapshot.data;
return Checkbox(
value: mayReportCrashes,
onChanged: (value) {
setState(() {
AppBloc().setMayReportCrashes(value);
});
},
);
},
),
Flexible(
child: Text(
'Allow Pattle to send crash reports to help development',
),
),
],
),
StreamBuilder<RequestState>(
stream: bloc.loginStream,
builder: (
......@@ -147,7 +178,7 @@ class PasswordPageState extends State<PasswordPage> {
child: child,
);
},
)
),
],
),
),
......
......@@ -25,16 +25,26 @@ import 'package:url/url.dart';
import 'dart:io';
import 'package:pedantic/pedantic.dart';
final bloc = StartBloc();
import '../../app_bloc.dart';
import '../bloc.dart';
typedef Request = void Function(Function addError);
typedef Check = bool Function(Function addError);
class StartBloc {
StartBloc() {
class StartBloc extends Bloc {
static StartBloc _instance;
StartBloc._() {
setHomeserverUrl("https://matrix.org");
}
factory StartBloc({bool replace = false}) {
if (_instance == null || replace) {
_instance = StartBloc._();
}
return _instance;
}
final _homeserverChangedSubj = BehaviorSubject<bool>();
Observable<bool> get homeserverChanged => _homeserverChangedSubj.stream;
......@@ -187,11 +197,19 @@ class StartBloc {
.login(_username, password, store: di.getStore())
.then((user) {
di.registerLocalUser(user);
AppBloc().notifyLogin();
_loginSubj.add(RequestState.success);
}).catchError((error) => _loginSubj.addError(error));
},
);
}
@override
void dispose() {
_homeserverChangedSubj.close();
_loginSubj.close();
_isUsernameAvailableSubj.close();
}
}
class RequestState {
......
......@@ -306,7 +306,7 @@ packages:
source: hosted
version: "1.5.5"
sqflite:
dependency: transitive
dependency: "direct main"
description:
name: sqflite
url: "https://pub.dartlang.org"
......
......@@ -15,6 +15,8 @@ dependencies:
matrix_sdk: ^0.20.3
matrix_sdk_sqflite: ^0.16.0
sqflite: ^1.1.6
rxdart: ^0.22.0
# TODO: Use official package when PR is merged
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment