Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/add event collection limits #5

Merged
merged 5 commits into from
May 10, 2024
Merged
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
5 changes: 5 additions & 0 deletions build/native_assets/linux/native_assets.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
format-version:
- 1
- 0
- 0
native-assets: {}
Binary file not shown.
Binary file added build/unit_test_assets/AssetManifest.bin
Binary file not shown.
1 change: 1 addition & 0 deletions build/unit_test_assets/AssetManifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions build/unit_test_assets/FontManifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
Binary file added build/unit_test_assets/NOTICES.Z
Binary file not shown.
Binary file added build/unit_test_assets/shaders/ink_sparkle.frag
Binary file not shown.
79 changes: 79 additions & 0 deletions lib/src/core/event_validator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import 'package:dart_mp_analytics/src/core/reserved_events.dart';

/// A helper class to validate event names and parameters.
class EventValidator {
/// Validates the given event name.
///
/// Returns `true` if the event name is valid, `false` otherwise.
///
/// Throws an assertion error if the event name is reserved, too long, or
/// empty, or if it contains invalid characters.
bool validateEventName(String eventName) {
assert(
!reservedEvents.contains(eventName),
'Event name $eventName is reserved and cannot be used',
);

if (reservedEvents.contains(eventName)) return false;

assert(
eventName.length <= 40,
'Event name $eventName is too long, must be 40 characters or less',
);

if (eventName.length > 40) return false;

assert(
eventName.isNotEmpty,
'Event name $eventName cannot be empty',
);

if (eventName.isEmpty) return false;

assert(
RegExp(r'^[a-zA-Z][a-zA-Z0-9_]*$').hasMatch(eventName),
'Event name $eventName must start with a letter and only contain '
'letters, numbers, and underscores',
);

if (!RegExp(r'^[a-zA-Z][a-zA-Z0-9_]*$').hasMatch(eventName)) return false;

return true;
}

/// Validates the given event parameters.
///
/// Returns `true` if the event parameters are valid, `false` otherwise.
///
/// Throws an assertion error if the event parameters contain more than 25
/// entries, if any parameter key is too long, or if any parameter value is
/// too long.
bool validateEventParameters(Map<String, Object?> parameters) {
assert(
parameters.length <= 25,
'Event parameters must be 25 or fewer',
);

if (parameters.length > 25) return false;

assert(
parameters.keys.every((key) => key.length <= 40),
'Event parameter keys must be 40 characters or less',
);

if (!parameters.keys.every((key) => key.length <= 40)) return false;

assert(
parameters.values
.whereType<String>()
.every((value) => value.length <= 100),
'Event parameter values must be 100 characters or less',
);

if (!parameters.values
.whereType<String>()
.every((value) => value.length <= 100)) return false;

return true;
}
}
52 changes: 52 additions & 0 deletions lib/src/core/reserved_events.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/// Google Analytics Automatically collected events, they are forbidden for use
/// https://support.google.com/analytics/answer/9234069
const Set<String> reservedEvents = {
'ad_activeview',
'ad_click',
'ad_exposure',
'ad_impression',
'ad_query',
'ad_reward',
'adunit_exposure',
'app_background',
'app_clear_data',
'app_exception',
'app_remove',
'app_store_refund',
'app_store_subscription_cancel',
'app_store_subscription_convert',
'app_store_subscription_renew',
'app_uninstall',
'app_update',
'app_upgrade',
'click',
'dynamic_link_app_open',
'dynamic_link_app_update',
'dynamic_link_first_open',
'error',
'file_download',
'firebase_campaign',
'firebase_in_app_message_action',
'firebase_in_app_message_dismiss',
'firebase_in_app_message_impression',
'first_open',
'first_visit',
'form_start',
'form_submit',
'in_app_purchase',
'notification_dismiss',
'notification_foreground',
'notification_open',
'notification_receive',
'os_update',
'page_view',
'screen_view',
'scroll',
'session_start',
'session_start_with_rollout',
'user_engagement',
'video_complete',
'video_progress',
'video_start',
'view_search_results',
};
52 changes: 52 additions & 0 deletions lib/src/core/user_data_validator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/// A helper class to validate user analytics data.
class UserDataValidator {
/// Returns `true` if the user properties can be added, `false` otherwise.
bool canAddUserProperty(Map<String, Object?> userProperties) {
assert(
userProperties.length < 25,
'User properties length must be less than 25',
);

return userProperties.length < 25;
}

/// Validates the given user property.
///
/// Returns `true` if the user properties are valid, `false` otherwise.
///
/// Throws an assertion error if the user property key or value is too long.
bool validateUserProperty(MapEntry<String, Object> userProperty) {
final key = userProperty.key;
final value = userProperty.value;

assert(
key.length <= 24,
'User property key $key is too long, must be 24 characters or less',
);

assert(
value.toString().length <= 36,
'User property value $value is too long, must be 36 characters or less',
);

if (key.length > 24 || value.toString().length > 36) return false;

return true;
}

/// Validates the given user ID.
///
/// Returns `true` if the user ID is valid, `false` otherwise.
///
/// Throws an assertion error if the user ID is too long.
bool validateUserId(String id) {
assert(
id.length <= 256,
'User ID $id is too long, must be 256 characters or less',
);

if (id.length > 256) return false;

return true;
}
}
22 changes: 16 additions & 6 deletions lib/src/models/mp_analytics_user.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// ignore_for_file: use_setters_to_change_properties

import 'package:dart_mp_analytics/src/core/user_data_validator.dart';

///{@template mp_analytics_user}
/// A class that defines the user that will be added to all events.
///
Expand All @@ -12,6 +14,7 @@ class MPAnalyticsUser {

String _id = '';
final Map<String, Object?> _properties = {};
final UserDataValidator _validator = UserDataValidator();

/// Returns a [Map] of the user id and user properties that will be added to
/// all events.
Expand All @@ -23,8 +26,12 @@ class MPAnalyticsUser {
}

/// Sets the user id that will be added to all events.
void setId(String id) {
_id = id;
bool setId(String id) {
final isValid = _validator.validateUserId(id);

if (isValid) _id = id;

return isValid;
}

/// Removes the current user id if one exists.
Expand All @@ -33,10 +40,13 @@ class MPAnalyticsUser {
}

/// Sets a user property that will be added to all events.
void setProperty(String key, Object value) {
_properties[key] = {
'value': value,
};
bool setProperty(String key, Object value) {
final isValid = _validator.canAddUserProperty(_properties) &&
_validator.validateUserProperty(MapEntry(key, value));

if (isValid) _properties[key] = {'value': value};

return isValid;
}

/// Removes a user property if one exists.
Expand Down
37 changes: 30 additions & 7 deletions lib/src/mp_analytics.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:convert';

import 'package:dart_mp_analytics/src/core/event_validator.dart';
import 'package:dart_mp_analytics/src/models/mp_analytics_options.dart';
import 'package:dart_mp_analytics/src/models/mp_analytics_user.dart';
import 'package:dart_mp_analytics/src/mp_analytics_client.dart';
Expand Down Expand Up @@ -41,12 +42,10 @@ class MPAnalytics {
this.enabled = true,
this.verbose = false,
Logger? logger,
MPAnalyticsUser? user,
MPAnalyticsClient? client,
}) {
_initialize(
logger: logger,
user: user,
client: client,
);
}
Expand Down Expand Up @@ -84,9 +83,10 @@ class MPAnalytics {

late final Logger _logger;

final EventValidator _validator = EventValidator();

void _initialize({
MPAnalyticsClient? client,
MPAnalyticsUser? user,
Logger? logger,
}) {
_logger = logger ??
Expand All @@ -102,7 +102,7 @@ class MPAnalytics {
}

_verboseLog('Initializing MPAnalytics');
_user = user ?? MPAnalyticsUser();
_user = MPAnalyticsUser();
_client = client ??
MPAnalyticsClient(
urlParameters: options.urlParameters,
Expand Down Expand Up @@ -135,7 +135,13 @@ class MPAnalytics {
_verboseLog('MPAnalytics disabled, not setting user ID: $id');
return;
}
_user.setId(id);
final isValid = _user.setId(id);

if (!isValid) {
_verboseLog('Invalid user ID, not setting user ID: $id');
return;
}

_verboseLog('Set user ID: $id');
}

Expand All @@ -157,11 +163,18 @@ class MPAnalytics {
void setUserProperty(String key, Object value) {
if (!enabled) {
_verboseLog(
'MPAnalytics disabled, not setting user property: $key = $value',
'MPAnalytics disabled, not setting user property: {$key : $value}',
);
return;
}
final isValid = _user.setProperty(key, value);

if (!isValid) {
_verboseLog(
'Invalid user property, not setting user property: {$key : $value}',
);
return;
}
_user.setProperty(key, value);
_verboseLog('Set user property: $key = $value');
}

Expand Down Expand Up @@ -193,6 +206,16 @@ class MPAnalytics {
return;
}

final (isValidEventName, isValidEventParameter) = (
_validator.validateEventName(name),
_validator.validateEventParameters(parameters),
);

if (!isValidEventName || !isValidEventParameter) {
_verboseLog('Invalid event name or parameters, not logging event: $name');
return;
}

final metadata = <String, Object?>{
for (final service in metadataServiceList) ...await service.getMetadata(),
};
Expand Down
Loading
Loading