Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions packages/core/lib/plugins/segment_destination.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,22 +30,31 @@ class SegmentDestination extends DestinationPlugin with Flushable {
final List<RawEvent> sentEvents = [];
var numFailedEvents = 0;

// FIXED: Only dequeue successfully sent events
await Future.forEach(chunkedEvents, (batch) async {
try {
final succeeded = await analytics?.httpClient.startBatchUpload(
analytics!.state.configuration.state.writeKey, batch,
host: _apiHost);
if (succeeded == null || !succeeded) {

if (succeeded == true) {
// Only add to sentEvents on actual success
sentEvents.addAll(batch);
} else {
numFailedEvents += batch.length;
}
sentEvents.addAll(batch);
} catch (e) {
numFailedEvents += batch.length;
} finally {
_queuePlugin.dequeue(sentEvents);
// Don't add failed events to sentEvents
}
// Move dequeue outside finally, after the loop
});

// Only dequeue events that were actually sent successfully
if (sentEvents.isNotEmpty) {
_queuePlugin.dequeue(sentEvents);
}

if (sentEvents.isNotEmpty) {
log("Sent ${sentEvents.length} events", kind: LogFilterKind.debug);
}
Expand Down
71 changes: 63 additions & 8 deletions packages/core/lib/utils/store/io.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,28 @@ import 'package:path_provider/path_provider.dart';

class StoreImpl with Store {
final bool storageJson;
StoreImpl({this.storageJson = true});
late final Future<void> _migrationCompleted;

StoreImpl({this.storageJson = true}) {
// Start migration immediately but don't block construction
_migrationCompleted = _migrateFilesFromDocumentsToSupport();
}
@override
Future get ready => Future.value();

@override
Future<Map<String, dynamic>?> getPersisted(String key) {
Future<Map<String, dynamic>?> getPersisted(String key) async {
if (!storageJson) return Future.value(null);
// Ensure migration is complete before reading files
await _migrationCompleted;
return _readFile(key);
}

@override
Future get ready => Future.value();

@override
Future setPersisted(String key, Map<String, dynamic> value) {
Future setPersisted(String key, Map<String, dynamic> value) async {
if (!storageJson) return Future.value();
// Ensure migration is complete before writing files
await _migrationCompleted;
return _writeFile(key, value);
}

Expand Down Expand Up @@ -70,7 +79,7 @@ class StoreImpl with Store {
}

Future<String> _fileName(String fileKey) async {
final path = (await _getDocumentDir()).path;
final path = (await _getNewDocumentDir()).path;
return "$path/analytics-flutter-$fileKey.json";
}

Expand All @@ -88,14 +97,60 @@ class StoreImpl with Store {
}
}

Future<Directory> _getDocumentDir() async {
Future<Directory> _getNewDocumentDir() async {
try {
return await getApplicationSupportDirectory();
Copy link
Preview

Copilot AI Sep 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change modifies where application data is stored, which could be a breaking change for existing users. Consider implementing a migration strategy to move existing data from the documents directory to the application support directory, or provide a configuration option to maintain backward compatibility.

Suggested change
return await getApplicationSupportDirectory();
final newDir = await getApplicationSupportDirectory();
final oldDir = await getApplicationDocumentsDirectory();
// Migrate old files if they exist
final oldFiles = oldDir
.listSync()
.whereType<File>()
.where((file) => file.path.contains('analytics-flutter-') && file.path.endsWith('.json'));
for (final oldFile in oldFiles) {
final fileName = oldFile.uri.pathSegments.last;
final newFile = File('${newDir.path}/$fileName');
if (!await newFile.exists()) {
await oldFile.copy(newFile.path);
await oldFile.delete();
}
}
return newDir;

Copilot uses AI. Check for mistakes.

} catch (err) {
throw PlatformNotSupportedError();
}
}

Future<Directory> _getOldDocumentDir() async {
try {
return await getApplicationDocumentsDirectory();
} catch (err) {
throw PlatformNotSupportedError();
}
}

/// Migrates existing analytics files from Documents directory to Application Support directory
Future<void> _migrateFilesFromDocumentsToSupport() async {
try {
final oldDir = await _getOldDocumentDir();
final newDir = await _getNewDocumentDir();

// List all analytics files in the old directory
final oldDirFiles = oldDir.listSync()
.whereType<File>()
.where((file) => file.path.contains('analytics-flutter-') && file.path.endsWith('.json'))
.toList();

for (final oldFile in oldDirFiles) {
final fileName = oldFile.path.split('/').last;
Copy link
Preview

Copilot AI Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using string splitting with hardcoded '/' is fragile across platforms. Use path.basename(oldFile.path) from the path package instead, which handles path separators correctly on all platforms.

Copilot uses AI. Check for mistakes.

final newFilePath = '${newDir.path}/$fileName';
final newFile = File(newFilePath);

// Only migrate if the file doesn't already exist in the new location
if (!await newFile.exists()) {
try {
// Ensure the new directory exists
await newDir.create(recursive: true);

// Copy the file to the new location
await oldFile.copy(newFilePath);

// Delete the old file after successful copy
await oldFile.delete();
} catch (e) {
// The app should continue to work even if migration fails
}
}
}
} catch (e) {
// Migration failure shouldn't break the app
Comment on lines +144 to +150
Copy link
Preview

Copilot AI Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The catch block silently ignores all exceptions during file operations. Consider logging the error for debugging purposes while still allowing the app to continue.

Suggested change
} catch (e) {
// The app should continue to work even if migration fails
}
}
}
} catch (e) {
// Migration failure shouldn't break the app
} catch (e, stackTrace) {
// The app should continue to work even if migration fails
print('File migration error: \$e');
print(stackTrace);
}
}
}
} catch (e, stackTrace) {
// Migration failure shouldn't break the app
print('Migration failure: \$e');
print(stackTrace);

Copilot uses AI. Check for mistakes.

Comment on lines +145 to +150
Copy link
Preview

Copilot AI Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The catch block silently ignores all exceptions during migration. Consider logging the error for debugging purposes while still allowing the app to continue.

Suggested change
// The app should continue to work even if migration fails
}
}
}
} catch (e) {
// Migration failure shouldn't break the app
// The app should continue to work even if migration fails
print('Error migrating file ${oldFile.path} to $newFilePath: $e');
}
}
}
} catch (e) {
// Migration failure shouldn't break the app
print('Error during migration from Documents to Support directory: $e');

Copilot uses AI. Check for mistakes.

}
}

@override
void dispose() {}
}