Skip to content

Commit

Permalink
Merge pull request #207 from Countly/internal-limits
Browse files Browse the repository at this point in the history
Internal limits
  • Loading branch information
turtledreams authored Apr 5, 2024
2 parents 87fd69f + 2a04f01 commit 579a4c2
Show file tree
Hide file tree
Showing 15 changed files with 826 additions and 15 deletions.
44 changes: 31 additions & 13 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,34 @@
## xx.x.x
* Fixed an issue where the 'reportFeedbackWidgetManually' function would await indefinitely on iOS
* Resolved an issue where nonfatal exceptions were treated as fatal and vice versa

* Underlying Android SDK version is 24.1.1
* Underlying iOS SDK version is 24.1.0

## xx.x.x-np
* Fixed an issue where the 'reportFeedbackWidgetManually' function would await indefinitely on iOS
* Resolved an issue where nonfatal exceptions were treated as fatal and vice versa

* Underlying Android SDK version is 24.1.1
* Underlying iOS SDK version is 24.1.0
## 24.4.0
* Mitigated an issue where the 'reportFeedbackWidgetManually' function would await indefinitely on iOS
* Mitigated an issue where nonfatal exceptions were treated as fatal and vice versa
* Mitigated an issue that caused session duration inconsistencies

* Added six new configuration options under the 'sdkInternalLimits' interface of 'CountlyConfig':
* 'setMaxKeyLength' for limiting the maximum size of all user provided string keys
* 'setMaxValueSize' for limiting the size of all values in user provided segmentation key-value pairs
* 'setMaxSegmentationValues' for limiting the max amount of user provided segmentation key-value pair count in one event
* 'setMaxBreadcrumbCount' for limiting the max amount of breadcrumbs that can be recorded before the oldest one is deleted
* 'setMaxStackTraceLinesPerThread' for limiting the max amount of stack trace lines to be recorded per thread
* 'setMaxStackTraceLineLength' for limiting the max characters allowed per stack trace lines

* Updated underlying Android SDK version to 24.4.0
* Updated underlying iOS SDK version to 24.4.0

## 24.4.0-np
* Mitigated an issue where the 'reportFeedbackWidgetManually' function would await indefinitely on iOS
* Mitigated an issue where nonfatal exceptions were treated as fatal and vice versa
* Mitigated an issue that caused session duration inconsistencies

* Added six new configuration options under the 'sdkInternalLimits' interface of 'CountlyConfig':
* 'setMaxKeyLength' for limiting the maximum size of all user provided string keys
* 'setMaxValueSize' for limiting the size of all values in user provided segmentation key-value pairs
* 'setMaxSegmentationValues' for limiting the max amount of user provided segmentation key-value pair count in one event
* 'setMaxBreadcrumbCount' for limiting the max amount of breadcrumbs that can be recorded before the oldest one is deleted
* 'setMaxStackTraceLinesPerThread' for limiting the max amount of stack trace lines to be recorded per thread
* 'setMaxStackTraceLineLength' for limiting the max characters allowed per stack trace lines

* Updated underlying Android SDK version to 24.4.0
* Updated underlying iOS SDK version to 24.4.0

## 24.1.1
* Added a new metric for detecting whether or not a device has a hinge for Android
Expand Down
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@ android {
}

dependencies {
implementation 'ly.count.android:sdk:24.1.1'
implementation 'ly.count.android:sdk:24.4.0'
implementation 'com.google.firebase:firebase-messaging:20.2.1'
}
Original file line number Diff line number Diff line change
Expand Up @@ -1189,6 +1189,7 @@ public void onFinished(JSONObject retrievedWidgetData, String error) {

String widgetId = widgetInfo.getString(0);

// TODO: for testing purposes we might want to bypass this check as it prevents us from using reportFeedbackWidgetManually directly
CountlyFeedbackWidget feedbackWidget = getFeedbackWidget(widgetId);
if (feedbackWidget == null) {
String errorMessage = "[reportFeedbackWidgetManually], No feedbackWidget is found against widget id : '" + widgetId + "' , always call 'getFeedbackWidgets' to get updated list of feedback widgets.";
Expand Down Expand Up @@ -1572,6 +1573,26 @@ private void populateConfig(JSONObject _config) throws JSONException {
this.config.setRecordAppStartTime(_config.getBoolean("recordAppStartTime"));
}
// APM END --------------------------------------------
// Internal Limits ------------------------------------
if (_config.has("maxKeyLength")) {
this.config.sdkInternalLimits.setMaxKeyLength(_config.getInt("maxKeyLength"));
}
if (_config.has("maxValueSize")) {
this.config.sdkInternalLimits.setMaxValueSize(_config.getInt("maxValueSize"));
}
if (_config.has("maxSegmentationValues")) {
this.config.sdkInternalLimits.setMaxSegmentationValues(_config.getInt("maxSegmentationValues"));
}
if (_config.has("maxBreadcrumbCount")) {
this.config.sdkInternalLimits.setMaxBreadcrumbCount(_config.getInt("maxBreadcrumbCount"));
}
if (_config.has("maxStackTraceLineLength")) {
this.config.sdkInternalLimits.setMaxStackTraceLineLength(_config.getInt("maxStackTraceLineLength"));
}
if (_config.has("maxStackTraceLinesPerThread")) {
this.config.sdkInternalLimits.setMaxStackTraceLinesPerThread(_config.getInt("maxStackTraceLinesPerThread"));
}
// Internal Limits END --------------------------------

if (_config.has("enableUnhandledCrashReporting") && _config.getBoolean("enableUnhandledCrashReporting")) {
this.config.enableCrashReporting();
Expand Down
2 changes: 1 addition & 1 deletion example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.countly.demo"
minSdkVersion flutter.minSdkVersion
minSdkVersion 21
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import 'dart:convert';
import 'dart:io';

import 'package:countly_flutter/countly_flutter.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../utils.dart';

/// Check if setting setMaxKeyLength to 1 truncates the keys (except internal keys)
/// Tested keys are:
/// - Event names and Event segmentation keys
/// - View names and View segmentation keys
/// - Custom APM trace keys and their segmentation keys
/// - Custom Crash segmentation keys
/// - Global View segmentation keys
/// - Custom User Property names and their modifications (with mul, push, pull, set, increment, etc)
const int MAX_KEY_LENGTH = 1;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('Init SDK with setMaxKeyLength', (WidgetTester tester) async {
// Initialize the SDK
CountlyConfig config = CountlyConfig(SERVER_URL, APP_KEY).setLoggingEnabled(true);
config.sdkInternalLimits.setMaxKeyLength(MAX_KEY_LENGTH);
await Countly.initWithConfig(config);

// Create truncable events
await createTruncableEvents();

// Get request and event queues from native side
List<String> requestList = await getRequestQueue();
List<String> eventList = await getEventQueue();

// Some logs for debugging
print('RQ: $requestList');
print('EQ: $eventList');
print('RQ length: ${requestList.length}');
print('EQ length: ${eventList.length}');

expect(requestList.length, Platform.isIOS ? 8 : 7); // user properties and custom user properties are separately sent in iOS
expect(eventList.length, 0);

// TODO: refactor this part (mote to utils and make it more generic)
// 0: begin session
// 1: custom APM with segmentation
// 2: network Trace
// 3: custom fatality crash with segmentation
// 4: custom mortal crash with segmentation
// 5: custom view and events with segmentation
// 6: custom user properties
var a = 0;
for (var element in requestList) {
Map<String, List<String>> queryParams = Uri.parse('?' + element).queryParametersAll;
testCommonRequestParams(queryParams); // checks general params
if (a == 1) {
Map<String, dynamic> apm = json.decode(queryParams['apm']![0]);
expect(apm['name'], 'Trace'.substring(0, MAX_KEY_LENGTH));
} else if (a == 2) {
Map<String, dynamic> apm = json.decode(queryParams['apm']![0]);
expect(apm['name'], 'Network Trace'.substring(0, MAX_KEY_LENGTH));
} else if (a == 3 || a == 4) {
Map<String, dynamic> crash = json.decode(queryParams['crash']![0]);
expect(crash['_custom']['Cats'.substring(0, MAX_KEY_LENGTH)], '12345');
expect(crash['_custom']['Moose'.substring(0, MAX_KEY_LENGTH)], 'Deer');
expect(crash['_custom']['Moons'.substring(0, MAX_KEY_LENGTH)], '9.9866');
} else if (a == 5) {
// 0) Custom Event
Map<String, dynamic> event = json.decode(queryParams['events']![0]);
expect(event['key'], 'Event With Sum And Segment'.substring(0, MAX_KEY_LENGTH));
expect(event['segmentation']['Country'.substring(0, MAX_KEY_LENGTH)], 'Turkey');
expect(event['segmentation']['Age'.substring(0, MAX_KEY_LENGTH)], '28884');
// 1) View Start (legacy)
Map<String, dynamic> view = json.decode(queryParams['events']![1]);
expect(view['key'], '[CLY]_view');
expect(view['segmentation']['Cats'.substring(0, MAX_KEY_LENGTH)], '12345');
expect(view['segmentation']['Moons'.substring(0, MAX_KEY_LENGTH)], '9.9866');
expect(view['segmentation']['segment'], Platform.isIOS ? 'iOS' : 'Android');
expect(view['segmentation']['start'], '1');
expect(view['segmentation']['name'], 'HomePage'.substring(0, MAX_KEY_LENGTH));
expect(view['segmentation']['Camel'.substring(0, MAX_KEY_LENGTH)], 666);
expect(view['segmentation']['visit'], '1');
expect(view['segmentation']['NotCamel'], 'Deerz');
expect(view['segmentation']['Moose'], 'Deer');
// 2) View End (legacy)
view = json.decode(queryParams['events']![2]);
expect(view['key'], '[CLY]_view');
expect(view['segmentation']['segment'], Platform.isIOS ? 'iOS' : 'Android');
expect(view['segmentation']['name'], 'HomePage'.substring(0, MAX_KEY_LENGTH));
expect(view['segmentation']['Camel'.substring(0, MAX_KEY_LENGTH)], 666);
expect(view['segmentation']['NotCamel'], 'Deerz');
// 3) View Start (AutoStopped)
view = json.decode(queryParams['events']![3]);
expect(view['key'], '[CLY]_view');
expect(view['segmentation']['Cats'.substring(0, MAX_KEY_LENGTH)], 12345);
expect(view['segmentation']['Moons'.substring(0, MAX_KEY_LENGTH)], 9.9866);
expect(view['segmentation']['segment'], Platform.isIOS ? 'iOS' : 'Android');
expect(view['segmentation']['name'], 'hawk'.substring(0, MAX_KEY_LENGTH));
expect(view['segmentation']['Camel'.substring(0, MAX_KEY_LENGTH)], 666);
expect(view['segmentation']['visit'], '1');
expect(view['segmentation']['NotCamel'], 'Deerz');
expect(view['segmentation']['Moose'], 'Deer');
// 4) View End (AutoStopped)
view = json.decode(queryParams['events']![4]);
expect(view['key'], '[CLY]_view');
expect(view['segmentation']['segment'], Platform.isIOS ? 'iOS' : 'Android');
expect(view['segmentation']['name'], 'hawk'.substring(0, MAX_KEY_LENGTH));
expect(view['segmentation']['Camel'.substring(0, MAX_KEY_LENGTH)], 666);
expect(view['segmentation']['NotCamel'], 'Deerz');
} else if (a == 6) {
Map<String, dynamic> userDetails = json.decode(queryParams['user_details']![0]);
checkUnchangingUserPropeties(userDetails);
expect(userDetails['custom']['special_value'.substring(0, MAX_KEY_LENGTH)], 'something special');
expect(userDetails['custom']['not_special_value'.substring(0, MAX_KEY_LENGTH)], 'something special cooking');

// TODO: this should be in a==7 for ios
expect(userDetails['custom']['setProperty'.substring(0, MAX_KEY_LENGTH)], 'My Property');
}

// some logs for debugging
print('RQ.$a: $queryParams');
print('========================');
a++;
}
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import 'dart:convert';
import 'dart:io';

import 'package:countly_flutter/countly_flutter.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../utils.dart';

/// Check if setting setMaxValueSize to 1 truncates the values
/// Tested values are:
/// - Event and View segmentation values
/// - Custom Crash segmentation values
/// - Global View and Crash segmentation values
/// - Custom User Property values and their modifications (with mul, push, pull, set, increment, etc)
/// - User Profile named key (username, email, etc) values (except the "picture" field, which has a limit of 4096 chars)
/// - Breadcrumb value (text)
/// - TODO: Manual Feedback and Rating Widgets reporting fields
const int MAX_VALUE_SIZE = 1;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('Init SDK with setMaxValueSize', (WidgetTester tester) async {
// Initialize the SDK
CountlyConfig config = CountlyConfig(SERVER_URL, APP_KEY).setLoggingEnabled(true);
config.sdkInternalLimits.setMaxValueSize(MAX_VALUE_SIZE);
await Countly.initWithConfig(config);

// Create truncable events
await createTruncableEvents();

// Get request and event queues from native side
List<String> requestList = await getRequestQueue();
List<String> eventList = await getEventQueue();

// Some logs for debugging
print('RQ: $requestList');
print('EQ: $eventList');
print('RQ length: ${requestList.length}');
print('EQ length: ${eventList.length}');

expect(requestList.length, Platform.isIOS ? 8 : 7); // user properties and custom user properties are separately sent in iOS
expect(eventList.length, 0);

// TODO: refactor this part (move to utils and make it more generic)
// 0: begin session
// 1: custom APM with segmentation
// 2: network Trace
// 3: custom fatality crash with segmentation
// 4: custom mortal crash with segmentation
// 5: custom view and events with segmentation
// 6: custom user properties
var a = 0;
for (var element in requestList) {
Map<String, List<String>> queryParams = Uri.parse('?' + element).queryParametersAll;
testCommonRequestParams(queryParams); // checks general params
if (a == 1) {
Map<String, dynamic> apm = json.decode(queryParams['apm']![0]);
expect(apm['name'], 'Trace');
expect(apm['apm_metrics']['C44CCC'], 1337);
expect(apm['apm_metrics']['ABCDEF'], 1233);
} else if (a == 2) {
Map<String, dynamic> apm = json.decode(queryParams['apm']![0]);
expect(apm['name'], 'Network Trace');
} else if (a == 3 || a == 4) {
Map<String, dynamic> crash = json.decode(queryParams['crash']![0]);
expect(crash['_custom']['Cats'], '12345'.substring(0, MAX_VALUE_SIZE));
expect(crash['_custom']['Moose'], 'Deer'.substring(0, MAX_VALUE_SIZE));
expect(crash['_custom']['Moons'], '9.9866'.substring(0, MAX_VALUE_SIZE));
expect(crash['_logs'], 'User Performed Step A'.substring(0, MAX_VALUE_SIZE) + '\n');
} else if (a == 5) {
// 0) Custom Event
List<dynamic> eventList = json.decode(queryParams['events']![0]);
var event = eventList[0];
expect(event['key'], 'Event With Sum And Segment');
expect(event['segmentation']['Country'], 'Turkey'.substring(0, MAX_VALUE_SIZE));
expect(event['segmentation']['Age'], '28884'.substring(0, MAX_VALUE_SIZE));
// 1) View Start (legacy)
var view = eventList[1];
expect(view['key'], '[CLY]_view');
expect(view['segmentation']['Cats'], '12345'.substring(0, MAX_VALUE_SIZE));
expect(view['segmentation']['Moons'], '9.9866'.substring(0, MAX_VALUE_SIZE));
expect(view['segmentation']['segment'], Platform.isIOS ? 'iOS' : 'Android');
expect(view['segmentation']['start'], '1');
expect(view['segmentation']['name'], 'HomePage');
expect(view['segmentation']['Camel'], 666);
expect(view['segmentation']['visit'], '1');
expect(view['segmentation']['NotCamel'], 'Deerz'.substring(0, MAX_VALUE_SIZE));
expect(view['segmentation']['Moose'], 'Deer'.substring(0, MAX_VALUE_SIZE));
// 2) View End (legacy)
view = eventList[2];
expect(view['key'], '[CLY]_view');
expect(view['segmentation']['segment'], Platform.isIOS ? 'iOS' : 'Android');
expect(view['segmentation']['name'], 'HomePage');
expect(view['segmentation']['Camel'], 666);
expect(view['segmentation']['NotCamel'], 'Deerz'.substring(0, MAX_VALUE_SIZE));
// 3) View Start (AutoStopped)
view = eventList[3];
expect(view['key'], '[CLY]_view');
expect(view['segmentation']['Cats'], 12345);
expect(view['segmentation']['Moons'], 9.9866);
expect(view['segmentation']['segment'], Platform.isIOS ? 'iOS' : 'Android');
expect(view['segmentation']['name'], 'hawk');
expect(view['segmentation']['Camel'], 666);
expect(view['segmentation']['visit'], '1');
expect(view['segmentation']['NotCamel'], 'Deerz'.substring(0, MAX_VALUE_SIZE));
expect(view['segmentation']['Moose'], 'Deer'.substring(0, MAX_VALUE_SIZE));
// 4) View End (AutoStopped)
view = eventList[4];
expect(view['key'], '[CLY]_view');
expect(view['segmentation']['segment'], Platform.isIOS ? 'iOS' : 'Android');
expect(view['segmentation']['name'], 'hawk');
expect(view['segmentation']['Camel'], 666);
expect(view['segmentation']['NotCamel'], 'Deerz'.substring(0, MAX_VALUE_SIZE));
} else if (a == 6) {
Map<String, dynamic> userDetails = json.decode(queryParams['user_details']![0]);
checkUnchangingUserPropeties(userDetails);
expect(userDetails['custom']['special_value'], 'something special'.substring(0, MAX_VALUE_SIZE));
expect(userDetails['custom']['not_special_value'], 'something special cooking'.substring(0, MAX_VALUE_SIZE));

// TODO: this should be in a==7 for ios
expect(userDetails['custom']['setProperty'], 'My Property'.substring(0, MAX_VALUE_SIZE));
}

// some logs for debugging
print('RQ.$a: $queryParams');
print('========================');
a++;
}
});
}
Loading

0 comments on commit 579a4c2

Please sign in to comment.