Skip to content

Commit b078fb7

Browse files
authored
Merge pull request #53 from powersync-ja/improved-errors
Improved error logging
2 parents df9d5f7 + 46664f8 commit b078fb7

File tree

21 files changed

+254
-94
lines changed

21 files changed

+254
-94
lines changed

demos/supabase-anonymous-auth/lib/main.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import './widgets/home_page.dart';
77
import './widgets/status_app_bar.dart';
88

99
void main() async {
10-
// Log info from PowerSync
1110
Logger.root.level = Level.INFO;
1211
Logger.root.onRecord.listen((record) {
1312
if (kDebugMode) {

demos/supabase-edge-function-auth/lib/main.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import './widgets/signup_page.dart';
99
import './widgets/status_app_bar.dart';
1010

1111
void main() async {
12-
// Log info from PowerSync
1312
Logger.root.level = Level.INFO;
1413
Logger.root.onRecord.listen((record) {
1514
if (kDebugMode) {

demos/supabase-todolist/lib/main.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import './widgets/signup_page.dart';
1313
import './widgets/status_app_bar.dart';
1414

1515
void main() async {
16-
// Log info from PowerSync
1716
Logger.root.level = Level.INFO;
1817
Logger.root.onRecord.listen((record) {
1918
if (kDebugMode) {

demos/supabase-todolist/lib/powersync.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,8 @@ Future<String> getDatabasePath() async {
151151

152152
Future<void> openDatabase() async {
153153
// Open the local database
154-
db = PowerSyncDatabase(schema: schema, path: await getDatabasePath());
154+
db = PowerSyncDatabase(
155+
schema: schema, path: await getDatabasePath(), logger: attachedLogger);
155156
await db.initialize();
156157

157158
await loadSupabase();

packages/powersync/lib/powersync.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
/// Use [PowerSyncDatabase] to open a database.
44
library;
55

6-
export 'src/powersync_database.dart';
7-
export 'src/schema.dart';
86
export 'src/connector.dart';
97
export 'src/crud.dart';
8+
export 'src/exceptions.dart';
9+
export 'src/log.dart';
10+
export 'src/open_factory.dart';
11+
export 'src/powersync_database.dart';
12+
export 'src/schema.dart';
1013
export 'src/sync_status.dart';
1114
export 'src/uuid.dart';
12-
export 'src/open_factory.dart';

packages/powersync/lib/src/bucket_storage.dart

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import 'dart:async';
22
import 'dart:convert';
33

44
import 'package:collection/collection.dart';
5+
import 'package:powersync/src/log_internal.dart';
56
import 'package:sqlite_async/mutex.dart';
67
import 'package:sqlite_async/sqlite3.dart' as sqlite;
78

89
import 'crud.dart';
910
import 'database_utils.dart';
10-
import 'log.dart';
1111
import 'schema_logic.dart';
1212
import 'sync_types.dart';
1313
import 'uuid.dart';
@@ -345,7 +345,7 @@ class BucketStorage {
345345
SET last_applied_op = last_op
346346
WHERE last_applied_op != last_op""");
347347

348-
log.fine('Updated local database');
348+
isolateLogger.fine('Applied checkpoint ${checkpoint.lastOpId}');
349349
return true;
350350
});
351351
}
@@ -407,7 +407,11 @@ class BucketStorage {
407407
final invalidBuckets = db.select(
408408
"SELECT name, target_op, last_op, last_applied_op FROM ps_buckets WHERE target_op > last_op AND (name = '\$local' OR pending_delete = 0)");
409409
if (invalidBuckets.isNotEmpty) {
410-
log.fine('Cannot update local database: $invalidBuckets');
410+
if (invalidBuckets.first['name'] == '\$local') {
411+
isolateLogger.fine('Waiting for local changes to be acknowledged');
412+
} else {
413+
isolateLogger.fine('Waiting for more data: $invalidBuckets');
414+
}
411415
return false;
412416
}
413417
// This is specifically relevant for when data is added to crud before another batch is completed.
@@ -505,8 +509,8 @@ class BucketStorage {
505509
BucketChecksum(bucket: checksum.bucket, checksum: 0, count: 0);
506510
// Note: Count is informational only.
507511
if (local.checksum != checksum.checksum) {
508-
log.warning(
509-
'Checksum mismatch for ${checksum.bucket}: local ${local.checksum} != remote ${checksum.checksum}');
512+
isolateLogger.warning(
513+
'Checksum mismatch for ${checksum.bucket}: local ${local.checksum} != remote ${checksum.checksum}. Likely due to sync rule changes.');
510514
failedChecksums.add(checksum.bucket);
511515
}
512516
}

packages/powersync/lib/src/connector.dart

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,13 @@ class PowerSyncCredentials {
8787
/// When the token expires. Only use for debugging purposes.
8888
final DateTime? expiresAt;
8989

90-
const PowerSyncCredentials(
90+
PowerSyncCredentials(
9191
{required this.endpoint,
9292
required this.token,
9393
this.userId,
94-
this.expiresAt});
94+
this.expiresAt}) {
95+
_validateEndpoint();
96+
}
9597

9698
factory PowerSyncCredentials.fromJson(Map<String, dynamic> parsed) {
9799
String token = parsed['token'];
@@ -133,6 +135,15 @@ class PowerSyncCredentials {
133135
Uri endpointUri(String path) {
134136
return Uri.parse(endpoint).resolve(path);
135137
}
138+
139+
_validateEndpoint() {
140+
final parsed = Uri.parse(endpoint);
141+
if ((!parsed.isScheme('http') && !parsed.isScheme('https')) ||
142+
parsed.host.isEmpty) {
143+
throw ArgumentError.value(
144+
endpoint, 'PowerSync endpoint must be a valid URL');
145+
}
146+
}
136147
}
137148

138149
/// Credentials used to connect to the PowerSync dev API.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import 'dart:async';
2+
import 'dart:convert' as convert;
3+
4+
import 'package:http/http.dart' as http;
5+
6+
/// This indicates an error with configured credentials.
7+
class CredentialsException implements Exception {
8+
String message;
9+
10+
CredentialsException(this.message);
11+
12+
@override
13+
toString() {
14+
return 'CredentialsException: $message';
15+
}
16+
}
17+
18+
/// An internal protocol exception.
19+
///
20+
/// This indicates that the server sent an invalid response.
21+
class PowerSyncProtocolException implements Exception {
22+
String message;
23+
24+
PowerSyncProtocolException(this.message);
25+
26+
@override
27+
toString() {
28+
return 'SyncProtocolException: $message';
29+
}
30+
}
31+
32+
/// An error that received from the sync service.
33+
///
34+
/// Examples include authorization errors (401) and temporary service issues (503).
35+
class SyncResponseException implements Exception {
36+
/// Parse an error response from the PowerSync service
37+
static Future<SyncResponseException> fromStreamedResponse(
38+
http.StreamedResponse response) async {
39+
try {
40+
final body = await response.stream.bytesToString();
41+
final decoded = convert.jsonDecode(body);
42+
final details = _stringOrFirst(decoded['error']?['details']) ?? body;
43+
final message = '${response.reasonPhrase ?? "Request failed"}: $details';
44+
return SyncResponseException(response.statusCode, message);
45+
} on Error catch (_) {
46+
return SyncResponseException(
47+
response.statusCode,
48+
response.reasonPhrase ?? "Request failed",
49+
);
50+
}
51+
}
52+
53+
/// Parse an error response from the PowerSync service
54+
static SyncResponseException fromResponse(http.Response response) {
55+
try {
56+
final body = response.body;
57+
final decoded = convert.jsonDecode(body);
58+
final details = _stringOrFirst(decoded['error']?['details']) ?? body;
59+
final message = '${response.reasonPhrase ?? "Request failed"}: $details';
60+
return SyncResponseException(response.statusCode, message);
61+
} on Error catch (_) {
62+
return SyncResponseException(
63+
response.statusCode,
64+
response.reasonPhrase ?? "Request failed",
65+
);
66+
}
67+
}
68+
69+
int statusCode;
70+
String description;
71+
72+
SyncResponseException(this.statusCode, this.description);
73+
74+
@override
75+
toString() {
76+
return 'SyncResponseException: $statusCode $description';
77+
}
78+
}
79+
80+
String? _stringOrFirst(Object? details) {
81+
if (details == null) {
82+
return null;
83+
} else if (details is String) {
84+
return details;
85+
} else if (details is List && details[0] is String) {
86+
return details[0];
87+
} else {
88+
return null;
89+
}
90+
}
Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,41 @@
11
import 'package:logging/logging.dart';
2+
import 'package:powersync/src/log_internal.dart';
23

3-
final log = Logger('PowerSync');
4+
/// Logger that outputs to the console in debug mode, and nothing
5+
/// in release and profile modes.
6+
final Logger autoLogger = _makeAutoLogger();
7+
8+
/// Logger that always outputs debug info to the console.
9+
final Logger debugLogger = _makeDebugLogger();
10+
11+
/// Standard logger. Does not automatically log to the console,
12+
/// use the `Logger.root.onRecord` stream to get log messages.
13+
final Logger attachedLogger = Logger('PowerSync');
14+
15+
Logger _makeDebugLogger() {
16+
// Use a detached logger to log directly to the console
17+
final logger = Logger.detached('PowerSync');
18+
logger.level = Level.FINE;
19+
logger.onRecord.listen((record) {
20+
print(
21+
'[${record.loggerName}] ${record.level.name}: ${record.time}: ${record.message}');
22+
23+
if (record.error != null) {
24+
print(record.error);
25+
}
26+
if (record.stackTrace != null) {
27+
print(record.stackTrace);
28+
}
29+
});
30+
return logger;
31+
}
32+
33+
Logger _makeAutoLogger() {
34+
if (kDebugMode) {
35+
return _makeDebugLogger();
36+
} else {
37+
final logger = Logger.detached('PowerSync');
38+
logger.level = Level.OFF;
39+
return logger;
40+
}
41+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import 'package:logging/logging.dart';
2+
3+
// Duplicate from package:flutter/foundation.dart, so we don't need to depend on Flutter
4+
const bool kReleaseMode = bool.fromEnvironment('dart.vm.product');
5+
const bool kProfileMode = bool.fromEnvironment('dart.vm.profile');
6+
const bool kDebugMode = !kReleaseMode && !kProfileMode;
7+
8+
// Implementation note: The loggers here are only initialized if used - it adds
9+
// no overhead when not used in the client app.
10+
11+
final isolateLogger = Logger.detached('PowerSync');

0 commit comments

Comments
 (0)