-
Notifications
You must be signed in to change notification settings - Fork 5
Hotfix: sync data-loss fixes, exchange-rate integrity, and observability (v1.0.6) #214
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f2a99b1
7c429cc
b5fa77f
82dab9e
83d0f6d
f01fa6d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,3 +1,6 @@ | ||||||||||||||||
| import 'dart:io'; | ||||||||||||||||
|
|
||||||||||||||||
| import 'package:dio/dio.dart'; | ||||||||||||||||
| import 'package:fpdart/fpdart.dart'; | ||||||||||||||||
| import 'package:trakli/core/error/exceptions.dart'; | ||||||||||||||||
| import 'package:trakli/core/error/failures/failures.dart'; | ||||||||||||||||
|
|
@@ -33,12 +36,33 @@ class RepositoryErrorHandler { | |||||||||||||||
| return left(DuplicateFailure(e.message)); | ||||||||||||||||
| } on NotFoundException { | ||||||||||||||||
| return left(const NotFoundFailure()); | ||||||||||||||||
| } on DioException catch (e, stackTrace) { | ||||||||||||||||
| if (isConnectivityError(e)) { | ||||||||||||||||
| return left(const NetworkFailure()); | ||||||||||||||||
| } | ||||||||||||||||
| logger.e('UnknownFailure', error: e, stackTrace: stackTrace); | ||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When a
Suggested change
|
||||||||||||||||
| return left(const UnknownFailure()); | ||||||||||||||||
| } catch (e, stackTrace) { | ||||||||||||||||
| logger.e('UnknownFailure', error: e, stackTrace: stackTrace); | ||||||||||||||||
| return left(const UnknownFailure()); | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| /// True when the request never reached the server (offline, DNS, timeout). | ||||||||||||||||
| static bool isConnectivityError(DioException e) { | ||||||||||||||||
| switch (e.type) { | ||||||||||||||||
| case DioExceptionType.connectionError: | ||||||||||||||||
| case DioExceptionType.connectionTimeout: | ||||||||||||||||
| case DioExceptionType.sendTimeout: | ||||||||||||||||
| case DioExceptionType.receiveTimeout: | ||||||||||||||||
| return true; | ||||||||||||||||
| case DioExceptionType.unknown: | ||||||||||||||||
| return e.error is SocketException; | ||||||||||||||||
| default: | ||||||||||||||||
| return false; | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| static ApiException mapFailureToException(Failure failure) { | ||||||||||||||||
| return failure.map( | ||||||||||||||||
| serverError: (ServerFailure f) => ServerException(f.message), | ||||||||||||||||
|
|
||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| import 'package:dio/dio.dart'; | ||
| import 'package:trakli/core/error/crash_reporting/crash_reporting_service.dart'; | ||
|
|
||
| /// Reports 5xx and 422 responses to Crashlytics as non-fatals, deduplicated | ||
| /// per method+path+status per session. | ||
| class CrashReportingInterceptor extends Interceptor { | ||
| CrashReportingInterceptor(this._crashReporting); | ||
|
|
||
| final CrashReportingService _crashReporting; | ||
| final Set<String> _reported = {}; | ||
|
|
||
| static const int _maxBodyLength = 2000; | ||
|
|
||
| bool shouldReport(int? status) => | ||
| status != null && (status >= 500 || status == 422); | ||
|
|
||
| /// Path with numeric/UUID segments replaced so the same endpoint | ||
| /// deduplicates across different record ids. | ||
| static String normalizePath(String path) { | ||
| return path | ||
| .split('/') | ||
| .map((s) => | ||
| RegExp(r'^(\d+|[0-9a-fA-F:-]{8,})$').hasMatch(s) ? '{id}' : s) | ||
| .join('/'); | ||
| } | ||
|
|
||
| @override | ||
| void onError(DioException err, ErrorInterceptorHandler handler) { | ||
| final status = err.response?.statusCode; | ||
| if (shouldReport(status)) { | ||
| final options = err.requestOptions; | ||
| final key = | ||
| '${options.method} ${normalizePath(options.uri.path)} $status'; | ||
| if (_reported.add(key)) { | ||
| final body = err.response?.data?.toString() ?? ''; | ||
| _crashReporting.recordError( | ||
| err, | ||
| stackTrace: err.stackTrace, | ||
| reason: 'HTTP $status on ${options.method} ${options.uri.path}', | ||
| information: { | ||
| 'url': options.uri.toString(), | ||
| 'status': status, | ||
| if (body.isNotEmpty) | ||
| 'response': body.length > _maxBodyLength | ||
| ? body.substring(0, _maxBodyLength) | ||
| : body, | ||
| }, | ||
| ); | ||
| } | ||
| } | ||
| handler.next(err); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import 'package:dio/dio.dart'; | ||
|
|
||
| /// Human-readable form of a sync failure for local_changes.error, carrying | ||
| /// the response body DioException.toString() omits. | ||
| String describeSyncError(Object error, {int maxBodyLength = 500}) { | ||
| if (error is DioException) { | ||
| final options = error.requestOptions; | ||
| final status = error.response?.statusCode; | ||
| final head = | ||
| 'HTTP ${status ?? 'error'} on ${options.method} ${options.uri.path}'; | ||
| final body = error.response?.data?.toString() ?? ''; | ||
| if (body.isEmpty) return '$head — ${error.message ?? error.type.name}'; | ||
| return '$head — ${body.length > maxBodyLength ? body.substring(0, maxBodyLength) : body}'; | ||
| } | ||
| return error.toString(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The method
_recordis not defined in this class. Based on the class definition and theCrashReportingInterface, you should callrecordErrorinstead. This is critical as it will cause a crash when a global error occurs.