Skip to content

Commit c14f09f

Browse files
authored
Merge pull request #556 from DevKor-github/feature/issue-546-runtime-app-metadata
Use runtime app metadata for version reporting
2 parents 1234dd6 + aa0f112 commit c14f09f

11 files changed

Lines changed: 210 additions & 53 deletions
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import 'package:injectable/injectable.dart';
2+
import 'package:package_info_plus/package_info_plus.dart';
3+
4+
class AppMetadata {
5+
const AppMetadata({required this.version, required this.buildNumber});
6+
7+
final String version;
8+
final String buildNumber;
9+
}
10+
11+
abstract interface class AppMetadataProvider {
12+
Future<AppMetadata> getMetadata();
13+
}
14+
15+
@Singleton(as: AppMetadataProvider)
16+
class PackageInfoAppMetadataProvider implements AppMetadataProvider {
17+
@override
18+
Future<AppMetadata> getMetadata() async {
19+
final packageInfo = await PackageInfo.fromPlatform();
20+
return AppMetadata(
21+
version: packageInfo.version,
22+
buildNumber: packageInfo.buildNumber,
23+
);
24+
}
25+
}

lib/core/services/product_analytics_service.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:firebase_analytics/firebase_analytics.dart';
22
import 'package:injectable/injectable.dart';
3+
import 'package:on_time_front/core/services/app_metadata_service.dart';
34
import 'package:on_time_front/core/services/device_info_service/shared.dart';
45
import 'package:on_time_front/domain/entities/analytics_preference.dart';
56
import 'package:on_time_front/domain/entities/product_usage_event.dart';
@@ -45,14 +46,17 @@ class FirebaseAnalyticsProviderClient implements AnalyticsProviderClient {
4546
class ProductAnalyticsService {
4647
ProductAnalyticsService({
4748
required AnalyticsProviderClient client,
49+
required AppMetadataProvider appMetadataProvider,
4850
@ignoreParam
4951
bool collectionAllowedInBuild = const bool.fromEnvironment(
5052
'ONTIME_ANALYTICS_ENABLED',
5153
),
5254
}) : _client = client,
55+
_appMetadataProvider = appMetadataProvider,
5356
_collectionAllowedInBuild = collectionAllowedInBuild;
5457

5558
final AnalyticsProviderClient _client;
59+
final AppMetadataProvider _appMetadataProvider;
5660
final bool _collectionAllowedInBuild;
5761
bool _collectionEnabled = false;
5862

@@ -66,11 +70,12 @@ class ProductAnalyticsService {
6670

6771
Future<void> track(ProductUsageEvent event) async {
6872
if (!_collectionEnabled) return;
73+
final metadata = await _appMetadataProvider.getMetadata();
6974
await _client.logEvent(
7075
name: event.name,
7176
parameters: event.toAnalyticsParameters(
7277
platform: _platformWireValue(),
73-
appVersion: '1.0.0',
78+
appVersion: metadata.version,
7479
),
7580
);
7681
}

lib/data/repositories/alarm_repository_impl.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:injectable/injectable.dart';
22
import 'package:on_time_front/core/services/alarm_scheduler_service.dart';
3+
import 'package:on_time_front/core/services/app_metadata_service.dart';
34
import 'package:on_time_front/core/services/device_info_service/shared.dart';
45
import 'package:on_time_front/core/validation/backend_constraints.dart';
56
import 'package:on_time_front/data/data_sources/alarm_remote_data_source.dart';
@@ -13,10 +14,12 @@ import 'package:uuid/uuid.dart';
1314
class AlarmRepositoryImpl implements AlarmRepository {
1415
final AlarmRemoteDataSource remoteDataSource;
1516
final AlarmSchedulerService schedulerService;
17+
final AppMetadataProvider appMetadataProvider;
1618

1719
AlarmRepositoryImpl({
1820
required this.remoteDataSource,
1921
required this.schedulerService,
22+
required this.appMetadataProvider,
2023
});
2124

2225
static const _deviceIdKey = 'alarm_device_id';
@@ -38,10 +41,11 @@ class AlarmRepositoryImpl implements AlarmRepository {
3841
@override
3942
Future<AlarmDeviceInfo> buildCurrentDeviceInfo() async {
4043
final capabilities = await schedulerService.getCapabilities();
44+
final metadata = await appMetadataProvider.getMetadata();
4145
return AlarmDeviceInfo(
4246
deviceId: await getDeviceId(),
4347
platform: _platformWireValue(),
44-
appVersion: '1.0.0',
48+
appVersion: metadata.version,
4549
osVersion: _osWireValue(),
4650
supportsNativeAlarm: capabilities.supportsNativeAlarm,
4751
nativeAlarmProvider: capabilities.nativeAlarmProvider,

macos/Flutter/GeneratedPluginRegistrant.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import flutter_appauth
1212
import flutter_local_notifications
1313
import flutter_secure_storage_darwin
1414
import google_sign_in_ios
15+
import package_info_plus
1516
import path_provider_foundation
1617
import shared_preferences_foundation
1718
import sign_in_with_apple
@@ -27,6 +28,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
2728
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
2829
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
2930
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
31+
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
3032
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
3133
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
3234
SignInWithApplePlugin.register(with: registry.registrar(forPlugin: "SignInWithApplePlugin"))

pubspec.lock

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -972,6 +972,22 @@ packages:
972972
url: "https://pub.dev"
973973
source: hosted
974974
version: "2.2.0"
975+
package_info_plus:
976+
dependency: "direct main"
977+
description:
978+
name: package_info_plus
979+
sha256: f5c435dc0e0d461e5b32471a870f769b6a1cc46930637efe24fbc535314e78ad
980+
url: "https://pub.dev"
981+
source: hosted
982+
version: "10.2.0"
983+
package_info_plus_platform_interface:
984+
dependency: transitive
985+
description:
986+
name: package_info_plus_platform_interface
987+
sha256: db762cb2f4f25ee60fb6359773861b0f199e00b90d237bd85a76a1e806b46ef4
988+
url: "https://pub.dev"
989+
source: hosted
990+
version: "4.1.0"
975991
path:
976992
dependency: transitive
977993
description:

pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ dependencies:
8787
sign_in_with_apple: ^8.1.0
8888
permission_handler: ^12.0.3
8989
firebase_analytics: ^12.4.3
90+
package_info_plus: ^10.2.0
9091

9192

9293

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import 'package:flutter_test/flutter_test.dart';
2+
import 'package:on_time_front/core/services/app_metadata_service.dart';
3+
import 'package:package_info_plus/package_info_plus.dart';
4+
5+
void main() {
6+
test('package info metadata provider returns runtime app metadata', () async {
7+
PackageInfo.setMockInitialValues(
8+
appName: 'OnTime',
9+
packageName: 'devkor.ontime',
10+
version: '9.8.7',
11+
buildNumber: '654',
12+
buildSignature: '',
13+
);
14+
15+
final metadata = await PackageInfoAppMetadataProvider().getMetadata();
16+
17+
expect(metadata.version, '9.8.7');
18+
expect(metadata.buildNumber, '654');
19+
});
20+
}

test/core/services/product_analytics_service_test.dart

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
import 'package:flutter_test/flutter_test.dart';
2+
import 'package:on_time_front/core/services/app_metadata_service.dart';
23
import 'package:on_time_front/core/services/product_analytics_service.dart';
34
import 'package:on_time_front/domain/entities/analytics_preference.dart';
45
import 'package:on_time_front/domain/entities/product_usage_event.dart';
56

67
void main() {
7-
test('unconfirmed analytics preference does not log product usage events', () async {
8+
test('product usage events include the runtime app version', () async {
89
final client = _FakeAnalyticsProviderClient();
910
final service = ProductAnalyticsService(
1011
client: client,
12+
appMetadataProvider: _FakeAppMetadataProvider(
13+
const AppMetadata(version: '9.8.7', buildNumber: '654'),
14+
),
1115
collectionAllowedInBuild: true,
1216
);
1317

1418
await service.applyPreference(
15-
const AnalyticsPreference(enabled: true, isConfirmed: false),
19+
const AnalyticsPreference(enabled: true, isConfirmed: true),
1620
);
1721
await service.track(
1822
const ProductUsageEvent(
@@ -22,9 +26,36 @@ void main() {
2226
),
2327
);
2428

25-
expect(client.collectionEnabledValues, [false]);
26-
expect(client.loggedEvents, isEmpty);
29+
expect(client.loggedEvents.single.parameters['app_version'], '9.8.7');
2730
});
31+
32+
test(
33+
'unconfirmed analytics preference does not log product usage events',
34+
() async {
35+
final client = _FakeAnalyticsProviderClient();
36+
final service = ProductAnalyticsService(
37+
client: client,
38+
appMetadataProvider: _FakeAppMetadataProvider(
39+
const AppMetadata(version: '9.8.7', buildNumber: '654'),
40+
),
41+
collectionAllowedInBuild: true,
42+
);
43+
44+
await service.applyPreference(
45+
const AnalyticsPreference(enabled: true, isConfirmed: false),
46+
);
47+
await service.track(
48+
const ProductUsageEvent(
49+
name: 'schedule_created',
50+
workflow: 'schedule',
51+
result: 'success',
52+
),
53+
);
54+
55+
expect(client.collectionEnabledValues, [false]);
56+
expect(client.loggedEvents, isEmpty);
57+
},
58+
);
2859
}
2960

3061
class _FakeAnalyticsProviderClient implements AnalyticsProviderClient {
@@ -47,3 +78,12 @@ class _FakeAnalyticsProviderClient implements AnalyticsProviderClient {
4778
@override
4879
Future<void> setUserId(String? userId) async {}
4980
}
81+
82+
class _FakeAppMetadataProvider implements AppMetadataProvider {
83+
const _FakeAppMetadataProvider(this.metadata);
84+
85+
final AppMetadata metadata;
86+
87+
@override
88+
Future<AppMetadata> getMetadata() async => metadata;
89+
}

test/data/repositories/alarm_repository_impl_test.dart

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:flutter_test/flutter_test.dart';
22
import 'package:on_time_front/core/services/alarm_scheduler_service.dart';
3+
import 'package:on_time_front/core/services/app_metadata_service.dart';
34
import 'package:on_time_front/data/data_sources/alarm_remote_data_source.dart';
45
import 'package:on_time_front/data/repositories/alarm_repository_impl.dart';
56
import 'package:on_time_front/domain/entities/alarm_entities.dart';
@@ -9,15 +10,20 @@ import 'package:shared_preferences/shared_preferences.dart';
910
void main() {
1011
late _FakeAlarmRemoteDataSource remoteDataSource;
1112
late _FakeAlarmSchedulerService schedulerService;
13+
late _FakeAppMetadataProvider appMetadataProvider;
1214
late AlarmRepositoryImpl repository;
1315

1416
setUp(() {
1517
SharedPreferences.setMockInitialValues({});
1618
remoteDataSource = _FakeAlarmRemoteDataSource();
1719
schedulerService = _FakeAlarmSchedulerService();
20+
appMetadataProvider = _FakeAppMetadataProvider(
21+
const AppMetadata(version: '9.8.7', buildNumber: '654'),
22+
);
1823
repository = AlarmRepositoryImpl(
1924
remoteDataSource: remoteDataSource,
2025
schedulerService: schedulerService,
26+
appMetadataProvider: appMetadataProvider,
2127
);
2228
});
2329

@@ -47,7 +53,7 @@ void main() {
4753
);
4854

4955
test(
50-
'buildCurrentDeviceInfo combines persisted id and scheduler capabilities',
56+
'buildCurrentDeviceInfo combines persisted id, scheduler capabilities, and runtime app version',
5157
() async {
5258
SharedPreferences.setMockInitialValues({
5359
'alarm_device_id': '123e4567-e89b-12d3-a456-426614174000',
@@ -61,7 +67,7 @@ void main() {
6167
final info = await repository.buildCurrentDeviceInfo();
6268

6369
expect(info.deviceId, '123e4567-e89b-12d3-a456-426614174000');
64-
expect(info.appVersion, '1.0.0');
70+
expect(info.appVersion, '9.8.7');
6571
expect(info.supportsNativeAlarm, isTrue);
6672
expect(info.nativeAlarmProvider, AlarmProvider.androidAlarmManager);
6773
expect(info.fallbackProvider, AlarmProvider.localNotification);
@@ -88,7 +94,7 @@ void main() {
8894
const deviceInfo = AlarmDeviceInfo(
8995
deviceId: 'device-1',
9096
platform: 'android',
91-
appVersion: '1.0.0',
97+
appVersion: '9.8.7',
9298
osVersion: 'android',
9399
supportsNativeAlarm: true,
94100
nativeAlarmProvider: AlarmProvider.androidAlarmManager,
@@ -137,6 +143,15 @@ class _FakeAlarmSchedulerService extends AlarmSchedulerService {
137143
Future<AlarmSchedulerCapabilities> getCapabilities() async => capabilities;
138144
}
139145

146+
class _FakeAppMetadataProvider implements AppMetadataProvider {
147+
const _FakeAppMetadataProvider(this.metadata);
148+
149+
final AppMetadata metadata;
150+
151+
@override
152+
Future<AppMetadata> getMetadata() async => metadata;
153+
}
154+
140155
class _FakeAlarmRemoteDataSource implements AlarmRemoteDataSource {
141156
AlarmSettings settings = const AlarmSettings(alarmsEnabled: true);
142157
final updatedValues = <bool>[];

0 commit comments

Comments
 (0)