• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

wger-project / flutter / 30854254088

03 Aug 2026 09:21PM UTC coverage: 40.895% (-15.0%) from 55.93%
30854254088

Pull #1260

github

web-flow
Merge d93794de5 into e3affdd4f
Pull Request #1260: WIP: health sync

9980 of 24404 relevant lines covered (40.89%)

1.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.59
/lib/core/errors.dart
1
/*
2
 * This file is part of wger Workout Manager <https://github.com/wger-project>.
3
 * Copyright (c) 2020 - 2026 wger Team
4
 *
5
 * wger Workout Manager is free software: you can redistribute it and/or modify
6
 * it under the terms of the GNU Affero General Public License as published by
7
 * the Free Software Foundation, either version 3 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 * GNU Affero General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU Affero General Public License
16
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
 */
18

19
import 'dart:async';
20
import 'dart:io';
21

22
import 'package:flutter/foundation.dart';
23
import 'package:flutter/painting.dart';
24
import 'package:http/http.dart' as http;
25

26
import 'consts.dart';
27

28
/// How an error should be surfaced to the user.
29
enum ErrorSeverity {
30
  /// Logged only; not worth interrupting the user (e.g. layout overflows).
31
  cosmetic,
32

33
  /// A brief, non-blocking snackbar (e.g. transient connectivity problems).
34
  transient,
35

36
  /// A blocking error dialog.
37
  fatal,
38
}
39

40
/// Classifies [error] to decide how it should be surfaced.
41
ErrorSeverity classifyError(Object? error) {
2✔
42
  // Flutter reports layout overflows as a plain FlutterError without a
43
  // dedicated type, so matching the message is the only option.
44
  final isLayoutOverflow = error is FlutterError && error.toString().contains('overflowed');
4✔
45

46
  // A failed network image load as a plain StateError
47
  final isImageLoadFailure = error is StateError && error.toString().contains('Failed to load');
4✔
48

49
  if (error is NetworkImageLoadException || isLayoutOverflow || isImageLoadFailure) {
2✔
50
    return ErrorSeverity.cosmetic;
51
  }
52
  if (error is SocketException || error is http.ClientException || error is TimeoutException) {
6✔
53
    return ErrorSeverity.transient;
54
  }
55
  return ErrorSeverity.fatal;
56
}
57

58
/// True if [e] means "the server can't be reached right now", as opposed to
59
/// an HTTP response we got but didn't like (e.g. a 401, which means the token
60
/// is invalid)
61
bool isNetworkError(Object e) {
2✔
62
  return e is http.ClientException ||
2✔
63
      e is SocketException ||
×
64
      e is HandshakeException ||
×
65
      e is TimeoutException;
×
66
}
67

68
/// Builds the URL that opens a pre-filled GitHub bug report.
69
///
70
/// All error-related parameters are optional so user-initiated reports (no
71
/// crash, just logs and diagnostics) render without empty error sections.
72
/// The details are passed to GitHub as query parameters and since GitHub
73
/// rejects URLs longer than [GITHUB_ISSUES_MAX_URL_LENGTH], an oversized
74
/// report first drops the oldest log entries and then, if still too long,
75
/// trims the stack trace from the bottom until the URL fits.
76
String buildGithubIssueUrl({
1✔
77
  required List<String> applicationLogs,
78
  String? issueTitle,
79
  String? issueErrorMessage,
80
  String? stackTrace,
81
  String? syncDiagnostics,
82
}) {
83
  final descriptionPrompt = issueErrorMessage != null
84
      ? '[Please describe what you were doing when the error occurred.]'
85
      : '[Please describe the problem you are seeing.]';
86

87
  String composeUrl(List<String> logs, String? trace) {
1✔
88
    final logText = logs.isEmpty ? '-- No logs available --' : logs.join('\n');
2✔
89
    final errorDetails = issueErrorMessage == null
90
        ? null
91
        : '## Error details\n\n'
1✔
92
              '${issueTitle != null ? 'Error title: $issueTitle\n' : ''}'
1✔
93
              'Error message: $issueErrorMessage'
94
              '${trace != null ? '\nStack trace:\n```\n$trace\n```' : ''}';
1✔
95
    final sections = [
1✔
96
      '## Description\n\n$descriptionPrompt',
1✔
97
      ?errorDetails,
1✔
98
      if (syncDiagnostics != null) 'Sync status:\n```\n$syncDiagnostics\n```',
×
99
      'App logs (last ${logs.length} entries):\n```\n$logText\n```',
2✔
100
    ];
101
    final description = sections.join('\n\n');
1✔
102
    return '$GITHUB_ISSUES_BUG_URL'
1✔
103
        '${issueTitle != null ? '&title=${Uri.encodeComponent(issueTitle)}' : ''}'
2✔
104
        '&description=${Uri.encodeComponent(description)}';
1✔
105
  }
106

107
  // The logs come newest-first, so the oldest entry is the last one. Once
108
  // all logs are gone, drop stack frames starting from the outermost one;
109
  // the top of the trace is where the error actually happened.
110
  var logs = applicationLogs;
111
  var trace = stackTrace;
112
  while (true) {
113
    final url = composeUrl(logs, trace);
1✔
114
    if (url.length <= GITHUB_ISSUES_MAX_URL_LENGTH) {
2✔
115
      return url;
116
    }
117
    if (logs.isNotEmpty) {
1✔
118
      logs = logs.sublist(0, logs.length - 1);
3✔
119
    } else if (trace != null && trace.contains('\n')) {
1✔
120
      trace = trace.substring(0, trace.lastIndexOf('\n'));
2✔
121
    } else {
122
      // Nothing left to trim
123
      return url;
124
    }
125
  }
126
}
127

128
class ApiError {
129
  final String key;
130
  late List<String> errorMessages = [];
131

132
  ApiError({required this.key, this.errorMessages = const []});
3✔
133

134
  @override
×
135
  String toString() {
136
    return 'ApiError(key: $key, errorMessage: $errorMessages)';
×
137
  }
138
}
139

140
/// Extracts error messages from the server response,
141
/// including nested error structures.
142
List<ApiError> extractErrors(Map<String, dynamic> errors) {
2✔
143
  final List<ApiError> errorList = [];
2✔
144
  _extractErrorsRecursive(errors, errorList);
2✔
145
  return errorList;
146
}
147

148
void _extractErrorsRecursive(dynamic errors, List<ApiError> errorList, [String? parentKey]) {
2✔
149
  if (errors is Map<String, dynamic>) {
2✔
150
    for (final key in errors.keys) {
4✔
151
      final value = errors[key];
2✔
152
      final fullKey = parentKey != null ? '$parentKey | ${_formatHeader(key)}' : key;
×
153
      _extractErrorsRecursive(value, errorList, fullKey);
2✔
154
    }
155
  } else if (errors is List) {
2✔
156
    // List of Maps (nested errors)
157
    if (errors.isNotEmpty && errors.first is Map<String, dynamic>) {
6✔
158
      for (final item in errors) {
×
159
        _extractErrorsRecursive(item, errorList, parentKey);
×
160
      }
161
    } else {
162
      // List of Strings
163
      final header = _formatHeader(parentKey ?? '');
2✔
164
      final error = ApiError(key: header, errorMessages: errors.cast<String>());
4✔
165
      errorList.add(error);
2✔
166
    }
167
  } else if (errors is String) {
×
168
    final header = _formatHeader(parentKey ?? '');
×
169
    final error = ApiError(key: header, errorMessages: [errors]);
×
170
    errorList.add(error);
×
171
  }
172
}
173

174
String _formatHeader(String key) {
2✔
175
  var header = key[0].toUpperCase() + key.substring(1, key.length);
10✔
176
  header = header.replaceAll('_', ' ');
2✔
177
  return header.replaceAll('.', ' ');
2✔
178
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc