Skip to content

Commit

Permalink
Add IP Address Caching
Browse files Browse the repository at this point in the history
This implements the ability to grab the users IP Address. This is used
for location tracking in the backend. We use the IP address to get a
loose Geo Location.
  • Loading branch information
anthonycastelli committed Jun 25, 2024
1 parent c8a08d1 commit ca7e23e
Show file tree
Hide file tree
Showing 8 changed files with 128 additions and 3 deletions.
11 changes: 8 additions & 3 deletions lib/appfit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,13 @@ class AppFit {
/// {@category Initialization}
static AppFit createInstance({
required AppFitConfiguration configuration,
EventDigester? eventDigester,
String instanceName = "default",
}) {
_instances ??= <String, AppFit>{};
return _instances!.putIfAbsent(
instanceName,
() => AppFit(configuration: configuration),
() => AppFit(configuration: configuration, eventDigester: eventDigester),
);
}

Expand Down Expand Up @@ -77,8 +78,12 @@ class AppFit {
AppFit({
required this.configuration,
EventDigester? eventDigester,
}) : eventDigester =
eventDigester ?? EventDigester(apiKey: configuration.apiKey) {
}) : eventDigester = eventDigester ??
EventDigester(
apiKey: configuration.apiKey,
appVersion: configuration.appVersion,
enableIpTracking: configuration.enableIpTracking,
) {
// Once we boot up the AppFit SDK, we need to generate an anonymousId
// and set the userId to null. This is to ensure that we have the most
// up-to-date information for the events.
Expand Down
7 changes: 7 additions & 0 deletions lib/appfit_configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,16 @@ class AppFitConfiguration {
/// The version of the app.
final String? appVersion;

/// IP Tracking
/// This allows you to enable/disable IP address tracking
///
/// Note: Disabling this will remove the location data from the event
final bool enableIpTracking;

/// Creates a new instance of [AppFitConfiguration].
AppFitConfiguration({
required this.apiKey,
this.appVersion,
this.enableIpTracking = true,
});
}
53 changes: 53 additions & 0 deletions lib/caching/ip_address.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import 'package:dio/dio.dart';

class IpAddress {
final String? address;
final DateTime lastUpdatedAt;

const IpAddress({
this.address,
required this.lastUpdatedAt,
});

bool get isExpired {
// Determine if the IP address is expired
// If the lastUpdatedAt is greater than 1 hour, then it's expired
return address == null ||
lastUpdatedAt.difference(DateTime.now()).inSeconds > 3600;
}

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is IpAddress &&
runtimeType == other.runtimeType &&
address == other.address;

@override
int get hashCode => address.hashCode;

@override
String toString() {
return "IpAddress(address: $address, lastUpdatedAt: $lastUpdatedAt)";
}

static Future<IpAddress> fetchIpAddress() async {
final response = await Dio().get("https://api.ipgeolocation.io/getip");
if (response.statusCode == null) {
return IpAddress(
address: null,
lastUpdatedAt: DateTime.now(),
);
}
if (response.statusCode! >= 200 && response.statusCode! < 300) {
return IpAddress(
address: response.data['ip'],
lastUpdatedAt: DateTime.now(),
);
}
return IpAddress(
address: null,
lastUpdatedAt: DateTime.now(),
);
}
}
20 changes: 20 additions & 0 deletions lib/networking/event_digester.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:async';
import 'package:appfit/appfit.dart';
import 'package:appfit/caching/appfit_cache.dart';
import 'package:appfit/caching/event_cache.dart';
import 'package:appfit/caching/ip_address.dart';
import 'package:appfit/networking/api_client.dart';
import 'package:appfit/networking/managers/system_manager.dart';
import 'package:appfit/networking/metric_event.dart';
Expand All @@ -16,6 +17,9 @@ class EventDigester {
/// The version of the app.
final String? appVersion;

/// Enable IP Tracking
final bool enableIpTracking;

/// The cache for the events.
final EventCache _cache = EventCache();

Expand All @@ -30,10 +34,17 @@ class EventDigester {
/// The API client for the AppFit dashboard.
late ApiClient _apiClient;

/// The IP address for the device.
IpAddress _ipAddress = IpAddress(
address: null,
lastUpdatedAt: DateTime.now(),
);

/// Initializes the [EventDigester] with the provided [apiKey] and [projectId].
EventDigester({
required this.apiKey,
this.appVersion,
this.enableIpTracking = true,
}) {
_apiClient = ApiClient(apiKey: apiKey);

Expand Down Expand Up @@ -114,6 +125,15 @@ class EventDigester {
systemProperties = systemProperties.copyWith(appVersion: appVersion);
}

if (enableIpTracking) {
if (_ipAddress.isExpired) {
_ipAddress = await IpAddress.fetchIpAddress();
}
systemProperties = systemProperties.copyWith(
ipAddress: _ipAddress.address,
);
}

return MetricEvent(
occurredAt: event.occurredAt,
payload: MetricPayload(
Expand Down
7 changes: 7 additions & 0 deletions lib/networking/properties/event_system_properties.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ class EventSystemProperties {
/// The version of the parent bundle
final String? appVersion;

/// The IP address of the device
final String? ipAddress;

/// All of the device related properties
/// These include anything that is specific to the physical device
/// such as model, operating system version, platform, etc
Expand All @@ -26,6 +29,7 @@ class EventSystemProperties {
/// Creates a new instance of [EventSystemProperties].
const EventSystemProperties({
this.appVersion,
this.ipAddress,
this.device,
this.browser,
this.operatingSystem,
Expand All @@ -34,6 +38,7 @@ class EventSystemProperties {
/// Creates a new instance of [EventSystemProperties] with the provided mutated properties
EventSystemProperties copyWith({
String? appVersion,
String? ipAddress,
DeviceProperties? device,
BrowserProperties? browser,
OperatingSystemProperties? operatingSystem,
Expand All @@ -51,6 +56,7 @@ class EventSystemProperties {
Map<String, dynamic> toJson() {
return {
'appVersion': appVersion,
'ipAddress': ipAddress,
'device': device?.toJson(),
'os': operatingSystem?.toJson(),
};
Expand All @@ -61,6 +67,7 @@ class EventSystemProperties {
factory EventSystemProperties.fromJson(Map<String, dynamic> json) {
return EventSystemProperties(
appVersion: json['appVersion'],
ipAddress: json['ipAddress'],
device: DeviceProperties.fromJson(json['device']),
operatingSystem: OperatingSystemProperties.fromJson(json['os']),
);
Expand Down
7 changes: 7 additions & 0 deletions test/appfit_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:path_provider_platform_interface/path_provider_platform_interfac
import 'package:shared_preferences/shared_preferences.dart';

import 'mocks/device_info_mocks.dart';
import 'mocks/mock_event_digester.dart';
import 'mocks/path_provider_mocks.dart';

void main() {
Expand All @@ -27,6 +28,7 @@ void main() {
test('initialization', () {
final appfit = AppFit(
configuration: AppFitConfiguration(apiKey: apiKey),
eventDigester: EventDigesterStub(),
);

expect(appfit.configuration.apiKey, apiKey);
Expand All @@ -35,6 +37,7 @@ void main() {
test('event tracking', () {
final appfit = AppFit(
configuration: AppFitConfiguration(apiKey: apiKey),
eventDigester: EventDigesterStub(),
);

appfit.trackEvent('test');
Expand All @@ -43,6 +46,7 @@ void main() {
test('should identify a valid userId', () {
final appfit = AppFit(
configuration: AppFitConfiguration(apiKey: apiKey),
eventDigester: EventDigesterStub(),
);

appfit.identifyUser('test');
Expand All @@ -51,6 +55,7 @@ void main() {
test('should identify a null userId', () {
final appfit = AppFit(
configuration: AppFitConfiguration(apiKey: apiKey),
eventDigester: EventDigesterStub(),
);

appfit.identifyUser(null);
Expand All @@ -67,12 +72,14 @@ void main() {
// The base 64 key is 'default:instance'
AppFit.createInstance(
configuration: AppFitConfiguration(apiKey: 'ZGVmYXVsdDppbnN0YW5jZQ=='),
eventDigester: EventDigesterStub(),
instanceName: 'default',
);

// The base 64 key is 'one:instance'
AppFit.createInstance(
configuration: AppFitConfiguration(apiKey: 'b25lOmluc3RhbmNl'),
eventDigester: EventDigesterStub(),
instanceName: 'one',
);
});
Expand Down
24 changes: 24 additions & 0 deletions test/mocks/mock_event_digester.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import 'package:appfit/appfit_event.dart';
import 'package:appfit/networking/event_digester.dart';

class EventDigesterStub implements EventDigester {
@override
final String apiKey = '';

@override
final String? appVersion = null;

@override
final bool enableIpTracking = false;

EventDigesterStub();

@override
Future<void> digest(AppFitEvent event) async {}

@override
Future<void> batchDigest(List<AppFitEvent> events) async {}

@override
Future<void> identify(String? userId) async {}
}
2 changes: 2 additions & 0 deletions test/mocks/system_manager_mocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ extension MockSystemManagerExt on MockSystemManager {
(_) => Future.value(
const EventSystemProperties(
appVersion: '1.0.0',
ipAddress: '98.234.193.126',
device: DeviceProperties(
manufacturer: 'Apple',
model: 'MacBookPro18,4',
Expand All @@ -33,6 +34,7 @@ extension MockSystemManagerExt on MockSystemManager {
(_) => Future.value(
const EventSystemProperties(
appVersion: '1.0.0',
ipAddress: '98.234.193.126',
device: DeviceProperties(
manufacturer: null,
model: null,
Expand Down

0 comments on commit ca7e23e

Please sign in to comment.