Skip to content

Commit

Permalink
Add support for System Properties
Browse files Browse the repository at this point in the history
This implements all of the logic for System Properties. This allows the SDK to fetch all of the system properties about the device, and browser. This ensures that we include basic device info for every event.
  • Loading branch information
anthonycastelli committed May 31, 2024
1 parent 8f133e5 commit de87a35
Show file tree
Hide file tree
Showing 18 changed files with 513 additions and 18 deletions.
14 changes: 7 additions & 7 deletions lib/networking/event_digester.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:appfit/appfit.dart';
import 'package:appfit/caching/appfit_cache.dart';
import 'package:appfit/caching/event_cache.dart';
import 'package:appfit/networking/api_client.dart';
import 'package:appfit/networking/managers/system_manager.dart';
import 'package:appfit/networking/metric_event.dart';
import 'package:appfit/networking/metric_payload.dart';

Expand All @@ -17,6 +18,11 @@ class EventDigester {
/// The cache for the AppFit SDK.
final AppFitCache _appFitCache = const AppFitCache();

/// The system manager for the AppFit SDK.
/// This is used to get the current system properties.
/// such as the device model, manufacturer, and operating system version.
final SystemManager _systemManager = const SystemManager();

/// The API client for the AppFit dashboard.
late ApiClient _apiClient;

Expand Down Expand Up @@ -97,13 +103,7 @@ class EventDigester {
Future<MetricEvent> _createRawEvent(AppFitEvent event) async {
final userId = await _appFitCache.getUserId();
final anonymousId = await _appFitCache.getAnonymousId();

// We are going to use the system properties to add the origin of the event.
// This will need to be reworked once we have the SDK actually
// fetching system properties
final systemProperties = {
'origin': 'flutter',
};
final systemProperties = await _systemManager.current();

return MetricEvent(
occurredAt: event.occurredAt,
Expand Down
109 changes: 109 additions & 0 deletions lib/networking/managers/device_manager.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import 'package:appfit/networking/properties/operating_system/operating_system_name.dart';
import 'package:device_info_plus/device_info_plus.dart';

class DeviceInfo {
final OperatingSystemName? operatingSystem;
final String? operatingSystemVersion;
final String? deviceManufacturer;
final String? deviceModel;
final String? userAgent;
final String? browserName;
final String? browserVersion;

const DeviceInfo({
this.operatingSystem,
this.operatingSystemVersion,
this.deviceManufacturer,
this.deviceModel,
this.userAgent,
this.browserName,
this.browserVersion,
});
}

class DeviceManager {
const DeviceManager();

Future<DeviceInfo?> current() async {
final deviceInfo = DeviceInfoPlugin();
switch (OperatingSystemName.current()) {
case OperatingSystemName.iOS:
final info = await deviceInfo.iosInfo;
return DeviceInfo(
operatingSystem: OperatingSystemName.iOS,
operatingSystemVersion: info.systemVersion,
deviceManufacturer: "Apple",
deviceModel: info.utsname.machine,
);

case OperatingSystemName.android:
final info = await deviceInfo.androidInfo;
return DeviceInfo(
operatingSystem: OperatingSystemName.android,
operatingSystemVersion: info.version.release,
deviceManufacturer: "Android",
deviceModel: info.model,
);

case OperatingSystemName.macOS:
final info = await deviceInfo.macOsInfo;
return DeviceInfo(
operatingSystem: OperatingSystemName.macOS,
operatingSystemVersion: info.osRelease,
deviceManufacturer: "Apple",
deviceModel: info.model,
);

case OperatingSystemName.windows:
// The required data for this is not available on windows.
// This will need to be updated in the future.
return null;

case OperatingSystemName.linux:
// The required data for this is not available on windows.
// This will need to be updated in the future.
return null;

case OperatingSystemName.web:
final info = await deviceInfo.webBrowserInfo;
return DeviceInfo(
operatingSystem: info.operatingSystem,
operatingSystemVersion: null,
deviceManufacturer: null,
deviceModel: null,
userAgent: info.userAgent,
browserName: info.browserName.name,
browserVersion: info.appVersion,
);

case OperatingSystemName.unknown:
return const DeviceInfo(
operatingSystem: OperatingSystemName.unknown,
operatingSystemVersion: null,
deviceManufacturer: "Unknown",
deviceModel: "Unknown",
);
}
}
}

extension OperatingSystem on WebBrowserInfo {
OperatingSystemName? get operatingSystem {
if (userAgent == null) return null;
if (userAgent!.contains("Mac")) {
return OperatingSystemName.macOS;
} else if (userAgent!.contains("iPad") ||
userAgent!.contains("iPhone") ||
userAgent!.contains("iPod")) {
return OperatingSystemName.iOS;
} else if (userAgent!.contains("Android")) {
return OperatingSystemName.android;
} else if (userAgent!.contains("Windows")) {
return OperatingSystemName.windows;
} else if (userAgent!.contains("Linux")) {
return OperatingSystemName.linux;
} else {
return OperatingSystemName.unknown;
}
}
}
48 changes: 48 additions & 0 deletions lib/networking/managers/system_manager.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import 'package:appfit/networking/managers/device_manager.dart';
import 'package:appfit/networking/properties/browser_properties.dart';
import 'package:appfit/networking/properties/device_properties.dart';
import 'package:appfit/networking/properties/event_system_properties.dart';
import 'package:appfit/networking/properties/operating_system/operating_system.dart';
import 'package:appfit/networking/properties/operating_system/operating_system_name.dart';
import 'package:flutter/foundation.dart';
import 'package:package_info_plus/package_info_plus.dart';

class SystemManager {
/// The device manager
/// This is used to get the current device properties
/// such as the device model, manufacturer, and operating system
/// version
@visibleForTesting
final DeviceManager deviceManager;

const SystemManager({
this.deviceManager = const DeviceManager(),
});

/// Returns the current properties of the system
Future<EventSystemProperties> current() async {
String? appVersion;
try {
final packageInfo = await PackageInfo.fromPlatform();
appVersion = packageInfo.version;
} catch (e) {
appVersion = null;
}
final manager = await deviceManager.current();
return EventSystemProperties(
appVersion: appVersion,
device: DeviceProperties(
manufacturer: manager?.deviceManufacturer,
model: manager?.deviceModel,
),
browser: BrowserProperties(
name: manager?.browserName,
version: manager?.browserVersion,
userAgent: manager?.userAgent,
),
operatingSystem: OperatingSystemProperties(
name: manager?.operatingSystem ?? OperatingSystemName.unknown,
version: manager?.operatingSystemVersion,
));
}
}
13 changes: 8 additions & 5 deletions lib/networking/metric_payload.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'package:appfit/networking/properties/event_system_properties.dart';

/// An event that is tracked by AppFit.
class MetricPayload {
/// The unique identifier for the event.
Expand Down Expand Up @@ -27,7 +29,7 @@ class MetricPayload {

/// The system properties of the event.
/// Note: These are automatically added by the SDK and should not be modified.
final Map<String, dynamic>? systemProperties;
final EventSystemProperties? systemProperties;

/// Creates a new instance of [MetricPayload].
const MetricPayload({
Expand All @@ -47,15 +49,14 @@ class MetricPayload {
String? userId,
String? anonymousId,
Map<String, String>? properties,
Map<String, String>? systemProperties,
}) {
return MetricPayload(
sourceEventId: sourceEventId ?? this.sourceEventId,
eventName: eventName ?? this.eventName,
userId: userId ?? this.userId,
anonymousId: anonymousId ?? this.anonymousId,
properties: properties ?? this.properties,
systemProperties: systemProperties ?? this.systemProperties,
systemProperties: systemProperties,
);
}

Expand All @@ -67,7 +68,9 @@ class MetricPayload {
userId: json['userId'],
anonymousId: json['anonymousId'],
properties: json['properties'],
systemProperties: json['systemProperties'],
systemProperties: json['systemProperties'] != null
? EventSystemProperties.fromJson(json['systemProperties'])
: null,
);
}

Expand All @@ -81,7 +84,7 @@ class MetricPayload {
'userId': userId,
'anonymousId': anonymousId,
'properties': properties,
'systemProperties': systemProperties,
'systemProperties': systemProperties?.toJson(),
};
}
}
31 changes: 31 additions & 0 deletions lib/networking/properties/browser_properties.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
class BrowserProperties {
final String? userAgent;
final String? name;
final String? version;

const BrowserProperties({
this.userAgent,
this.name,
this.version,
});

/// Converts the [BrowserProperties] to a JSON object.
/// This is used to convert an instance of [BrowserProperties] into a JSON object that can be sent to the server.
Map<String, dynamic> toJson() {
return {
'userAgent': userAgent,
'name': name,
'version': version,
};
}

/// Creates a new instance of [BrowserProperties] from a JSON object.
/// This is used to convert a JSON object from the server into an instance of [BrowserProperties].
factory BrowserProperties.fromJson(Map<String, dynamic> json) {
return BrowserProperties(
userAgent: json['userAgent'],
name: json['name'],
version: json['version'],
);
}
}
28 changes: 28 additions & 0 deletions lib/networking/properties/device_properties.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
class DeviceProperties {
/// The Device Manufacturer (Apple)
final String? manufacturer;

/// The Device Model (MacBookPro18,4)
final String? model;

const DeviceProperties({
required this.manufacturer,
required this.model,
});

/// Converts the [DeviceProperties] to a JSON object.
Map<String, dynamic> toJson() {
return {
'manufacturer': manufacturer,
'model': model,
};
}

/// Creates a new instance of [DeviceProperties] from a JSON object.
factory DeviceProperties.fromJson(Map<String, dynamic> json) {
return DeviceProperties(
manufacturer: json['manufacturer'],
model: json['model'],
);
}
}
53 changes: 53 additions & 0 deletions lib/networking/properties/event_system_properties.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import 'package:appfit/networking/properties/browser_properties.dart';
import 'package:appfit/networking/properties/device_properties.dart';
import 'package:appfit/networking/properties/operating_system/operating_system.dart';

/// Event System Properties
/// These return all of the system properties we generate based on the device type
/// and data we wish to always include in every event.
class EventSystemProperties {
/// The version of the parent bundle
final String? appVersion;

/// All of the device related properties
/// These include anything that is specific to the physical device
/// such as model, operating system version, platform, etc
final DeviceProperties? device;

/// All of the browser properties
/// These include the browser name, version, and user agent
final BrowserProperties? browser;

/// All of the operating system properties
/// These include the operating system name (i.e. iOS or macOS)
/// and the version number (i.e. 17.5.1)
final OperatingSystemProperties? operatingSystem;

/// Creates a new instance of [EventSystemProperties].
const EventSystemProperties({
this.appVersion,
this.device,
this.browser,
this.operatingSystem,
});

/// Converts the [EventSystemProperties] to a JSON object.
/// This is used to convert the object to a JSON object that can be sent to the server.
Map<String, dynamic> toJson() {
return {
'appVersion': appVersion,
'device': device?.toJson(),
'os': operatingSystem?.toJson(),
};
}

/// Creates a new instance of [EventSystemProperties] from a JSON object.
/// This is used to convert a JSON object from the server into an instance of [EventSystemProperties].
factory EventSystemProperties.fromJson(Map<String, dynamic> json) {
return EventSystemProperties(
appVersion: json['appVersion'],
device: DeviceProperties.fromJson(json['device']),
operatingSystem: OperatingSystemProperties.fromJson(json['os']),
);
}
}
36 changes: 36 additions & 0 deletions lib/networking/properties/operating_system/operating_system.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import 'package:appfit/networking/properties/operating_system/operating_system_name.dart';

class OperatingSystemProperties {
/// Operating System Name
///
/// This returns the name of the operating system
/// Example: iOS, macOS, Windows, etc
final OperatingSystemName name;

/// Operating System Version
///
/// This will follow Semantic Versioning
/// so it will return a string similar to major.minor.path (i.e. 17.5.1)
final String? version;

const OperatingSystemProperties({
required this.name,
required this.version,
});

/// Converts the [OperatingSystemProperties] to a JSON object.
Map<String, dynamic> toJson() {
return {
'name': name.name,
'version': version,
};
}

/// Creates a new instance of [OperatingSystemProperties] from a JSON object.
factory OperatingSystemProperties.fromJson(Map<String, dynamic> json) {
return OperatingSystemProperties(
name: json['name'],
version: json['version'],
);
}
}
Loading

0 comments on commit de87a35

Please sign in to comment.