Skip to content

Commit

Permalink
delete old log file if it exceeds kMaxLogFileSize of 25MiB (#277)
Browse files Browse the repository at this point in the history
* delete old log file if it exceeds kMaxLogFileSize of 25MiB

* update changelog

* bump version in pubspec.yaml

* update constants.dart
  • Loading branch information
christopherfujino authored and andrewkolos committed Jun 25, 2024
1 parent ab5e2da commit 6d41e14
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 3 deletions.
1 change: 1 addition & 0 deletions pkgs/unified_analytics/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## 5.8.8+2

- Avoid opening large telemetry log files to prevent out of memory errors.
- Fixed bug where calling `Analytics.send` could result in a `FileSystemException` when unable to write to a log file.

## 5.8.8+1
Expand Down
5 changes: 5 additions & 0 deletions pkgs/unified_analytics/lib/src/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ const String kGoogleAnalyticsMeasurementId = 'G-04BXPVBCWJ';
/// How many data records to store in the log file.
const int kLogFileLength = 2500;

/// The maximum allowed size of the telemetry log file.
///
/// 25 MiB.
const int kMaxLogFileSize = 25 * (1 << 20);

/// Filename for the log file to persist sent events on user's machine.
const String kLogFileName = 'dart-flutter-telemetry.log';

Expand Down
14 changes: 11 additions & 3 deletions pkgs/unified_analytics/lib/src/log_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -282,10 +282,18 @@ class LogHandler {
/// This will keep the max number of records limited to equal to
/// or less than [kLogFileLength] records.
void save({required Map<String, Object?> data}) {
var records = logFile.readAsLinesSync();
final content = '${jsonEncode(data)}\n';

try {
final stat = logFile.statSync();
List<String> records;
if (stat.size > kMaxLogFileSize) {
logFile.deleteSync();
logFile.createSync();
records = [];
} else {
records = logFile.readAsLinesSync();
}
final content = '${jsonEncode(data)}\n';

// When the record count is less than the max, add as normal;
// else drop the oldest records until equal to max
if (records.length < kLogFileLength) {
Expand Down
92 changes: 92 additions & 0 deletions pkgs/unified_analytics/test/log_handler_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:convert';

import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:path/path.dart' as p;
import 'package:test/fake.dart';
import 'package:test/test.dart';

import 'package:unified_analytics/src/constants.dart';
Expand Down Expand Up @@ -221,6 +224,47 @@ void main() {
logHandler.save(data: {});
});

test('deletes log file larger than kMaxLogFileSize', () async {
var deletedLargeLogFile = false;
var wroteDataToLogFile = false;
const data = <String, Object?>{};
final logFile = _FakeFile('log.txt')
.._deleteSyncImpl = (() => deletedLargeLogFile = true)
.._createSyncImpl = () {}
.._statSyncImpl = (() => _FakeFileStat(kMaxLogFileSize + 1))
.._writeAsStringSync = (contents, {mode = FileMode.append}) {
expect(contents.trim(), data.toString());
expect(mode, FileMode.writeOnlyAppend);
wroteDataToLogFile = true;
};
final logHandler = LogHandler(logFile: logFile);

logHandler.save(data: data);
expect(deletedLargeLogFile, isTrue);
expect(wroteDataToLogFile, isTrue);
});

test('does not delete log file if smaller than kMaxLogFileSize', () async {
var wroteDataToLogFile = false;
const data = <String, Object?>{};
final logFile = _FakeFile('log.txt')
.._deleteSyncImpl =
(() => fail('called logFile.deleteSync() when file was less than '
'kMaxLogFileSize'))
.._createSyncImpl = () {}
.._readAsLinesSyncImpl = (() => ['three', 'previous', 'lines'])
.._statSyncImpl = (() => _FakeFileStat(kMaxLogFileSize - 1))
.._writeAsStringSync = (contents, {mode = FileMode.append}) {
expect(contents.trim(), data.toString());
expect(mode, FileMode.writeOnlyAppend);
wroteDataToLogFile = true;
};
final logHandler = LogHandler(logFile: logFile);

logHandler.save(data: data);
expect(wroteDataToLogFile, isTrue);
});

test('Catching cast errors for each log record silently', () async {
// Write a json array to the log file which will cause
// a cast error when parsing each line
Expand Down Expand Up @@ -301,3 +345,51 @@ void main() {
expect(newString, testString);
});
}

class _FakeFileStat extends Fake implements FileStat {
_FakeFileStat(this.size);

@override
final int size;
}

class _FakeFile extends Fake implements File {
_FakeFile(this.path);

List<String> Function()? _readAsLinesSyncImpl;

@override
List<String> readAsLinesSync({Encoding encoding = utf8}) =>
_readAsLinesSyncImpl!();

@override
final String path;

FileStat Function()? _statSyncImpl;

@override
FileStat statSync() => _statSyncImpl!();

void Function()? _deleteSyncImpl;

@override
void deleteSync({bool recursive = false}) => _deleteSyncImpl!();

void Function()? _createSyncImpl;

@override
void createSync({bool recursive = false, bool exclusive = false}) {
return _createSyncImpl!();
}

void Function(String contents, {FileMode mode})? _writeAsStringSync;

@override
void writeAsStringSync(
String contents, {
FileMode mode = FileMode.write,
Encoding encoding = utf8,
bool flush = false,
}) =>
_writeAsStringSync!(contents, mode: mode);
}

0 comments on commit 6d41e14

Please sign in to comment.