diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e1cd50b..5c697b90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,29 @@ +## 24.4.1 +* Added support for Feedback Widget terms and conditions +* 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 + +* Android Specific Changes: + * ! Minor breaking change ! Introduced SDK internal limits + * Mitigated an issue where the session duration could have been calculated wrongly after a device ID change without merge + * Mitigated an issue where a session could have continued after a device ID change without merge + +* iOS Specific Changes: + * Mitigated an issue where internal limits were not being applied to some values + * Mitigated an issue where SDK limits could affect internal keys + * Mitigated an issue that enabled recording reserved events + * Mitigated an issue where timed events could have no ID + * Mitigated an issue where the request queue could overflow while sending a request + * Removed timestamps from crash breadcrumbs + +* Updated the underlying Android SDK version to 24.4.1 +* Updated the underlying iOS SDK version to 24.4.1 + ## 24.4.0 * ! Minor breaking change ! Tracking of foreground and background time for APM is disabled by default diff --git a/Countly.d.ts b/Countly.d.ts index 015e1860..dd8d12c3 100644 --- a/Countly.d.ts +++ b/Countly.d.ts @@ -1129,6 +1129,44 @@ declare module "countly-sdk-react-native-bridge/CountlyConfig" { setAppStartTimestampOverride(timestamp: number): CountlyConfigApm; } + class CountlyConfigSDKInternalLimits { + /** + * Limits the maximum size of all string keys + * @param keyLengthLimit - maximum char size of all string keys (default 128 chars) + */ + setMaxKeyLength(keyLengthLimit: number) : CountlyConfigSDKInternalLimits; + + /** + * Limits the size of all values in segmentation key-value pairs + * @param valueSizeLimit - the maximum char size of all values in our key-value pairs (default 256 chars) + */ + setMaxValueSize(valueSizeLimit: number) : CountlyConfigSDKInternalLimits; + + /** + * Limits the max amount of custom segmentation in one event + * @param segmentationAmountLimit - the maximum amount of custom segmentation in one event (default 100 key-value pairs) + */ + setMaxSegmentationValues(segmentationAmountLimit: number) : CountlyConfigSDKInternalLimits; + + /** + * Limits the max amount of breadcrumbs that can be recorded before the oldest one is deleted + * @param breadcrumbCountLimit - the maximum amount of breadcrumbs that can be recorded before the oldest one is deleted (default 100) + */ + setMaxBreadcrumbCount(breadcrumbCountLimit: number) : CountlyConfigSDKInternalLimits; + + /** + * Limits the max amount of stack trace lines to be recorded per thread + * @param stackTraceLinesPerThreadLimit - maximum amount of stack trace lines to be recorded per thread (default 30) + */ + setMaxStackTraceLinesPerThread(stackTraceLinesPerThreadLimit: number) : CountlyConfigSDKInternalLimits; + + /** + * Limits the max characters allowed per stack trace lines. Also limits the crash message length + * @param stackTraceLineLengthLimit - maximum length of each stack trace line (default 200) + */ + setMaxStackTraceLineLength(stackTraceLineLengthLimit: number) : CountlyConfigSDKInternalLimits; + } + /** * * Config object for Countly Init @@ -1146,6 +1184,10 @@ declare module "countly-sdk-react-native-bridge/CountlyConfig" { * getter for CountlyConfigApm instance that is used to access CountlyConfigApm methods */ apm: CountlyConfigApm; + /** + * getter for CountlySDKLimits instance that is used to access CountlyConfigSDKInternalLimits methods + */ + sdkInternalLimits: CountlyConfigSDKInternalLimits; /** * Method to set the server url diff --git a/CountlyConfig.js b/CountlyConfig.js index 1f8395d7..5ef6c516 100644 --- a/CountlyConfig.js +++ b/CountlyConfig.js @@ -1,5 +1,6 @@ import { initialize } from "./Logger.js"; import CountlyConfigApm from "./lib/configuration_interfaces/countly_config_apm.js"; +import CountlyConfigSDKInternalLimits from "./lib/configuration_interfaces/countly_config_limits.js"; /** * Countly SDK React Native Bridge * https://github.com/Countly/countly-sdk-react-native-bridge @@ -18,6 +19,7 @@ class CountlyConfig { this.serverURL = serverURL; this.appKey = appKey; this._countlyConfigApmInstance = new CountlyConfigApm(); + this._countlyConfigSDKLimitsInstance = new CountlyConfigSDKInternalLimits(); } /** @@ -27,6 +29,10 @@ class CountlyConfig { return this._countlyConfigApmInstance; } + get sdkInternalLimits() { + return this._countlyConfigSDKLimitsInstance; + } + /** * Method to set the server url * diff --git a/CountlyReactNative.podspec b/CountlyReactNative.podspec index d32be47c..1688a4dd 100644 --- a/CountlyReactNative.podspec +++ b/CountlyReactNative.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'CountlyReactNative' - s.version = '24.4.0' + s.version = '24.4.1' s.license = { :type => 'COMMUNITY', :text => <<-LICENSE diff --git a/Utils.js b/Utils.js index 74f6bc86..37c7592a 100644 --- a/Utils.js +++ b/Utils.js @@ -17,19 +17,19 @@ const DeviceIdType = { function intToDeviceIDType(deviceIdType) { let result = null; switch (deviceIdType) { - case 10101: - result = DeviceIdType.SDK_GENERATED; - break; - case 20202: - result = DeviceIdType.DEVELOPER_SUPPLIED; - break; - case 30303: - result = DeviceIdType.TEMPORARY_ID; - break; - default: - L.e("_getDeviceIdType, " + `unexpected deviceIdType [${deviceIdType}] from native side`); - result = DeviceIdType.SDK_GENERATED; - break; + case 10101: + result = DeviceIdType.SDK_GENERATED; + break; + case 20202: + result = DeviceIdType.DEVELOPER_SUPPLIED; + break; + case 30303: + result = DeviceIdType.TEMPORARY_ID; + break; + default: + L.e("_getDeviceIdType, " + `unexpected deviceIdType [${deviceIdType}] from native side`); + result = DeviceIdType.SDK_GENERATED; + break; } L.d(`_getDeviceIdType, DeviceIDType: ${result}`); return result; @@ -134,6 +134,50 @@ function configToJson(config) { if (config.attributionValues) { json.attributionValues = config.attributionValues; } + // Limits ----------------------------------------------- + if (config.sdkInternalLimits.maxKeyLength) { + if (config.sdkInternalLimits.maxKeyLength < 1) { + L.w(`configToJson, Provided value for maxKeyLength is invalid!`) + } else { + json.maxKeyLength = config.sdkInternalLimits.maxKeyLength; + } + } + if (config.sdkInternalLimits.maxValueSize) { + if (config.sdkInternalLimits.maxValueSize < 1) { + L.w(`configToJson, Provided value for maxValueSize is invalid!`) + } else { + json.maxValueSize = config.sdkInternalLimits.maxValueSize; + } + } + if (config.sdkInternalLimits.maxSegmentationValues) { + if (config.sdkInternalLimits.maxSegmentationValues < 1) { + L.w(`configToJson, Provided value for maxSegmentationValues is invalid!`) + } else { + json.maxSegmentationValues = config.sdkInternalLimits.maxSegmentationValues; + } + } + if (config.sdkInternalLimits.maxBreadcrumbCount) { + if (config.sdkInternalLimits.maxBreadcrumbCount < 1) { + L.w(`configToJson, Provided value for maxBreadcrumbCount is invalid!`) + } else { + json.maxBreadcrumbCount = config.sdkInternalLimits.maxBreadcrumbCount; + } + } + if (config.sdkInternalLimits.maxStackTraceLinesPerThread) { + if (config.sdkInternalLimits.maxStackTraceLinesPerThread < 1) { + L.w(`configToJson, Provided value for maxStackTraceLinesPerThread is invalid!`) + } else { + json.maxStackTraceLinesPerThread = config.sdkInternalLimits.maxStackTraceLinesPerThread; + } + } + if (config.sdkInternalLimits.maxStackTraceLineLength) { + if (config.sdkInternalLimits.maxStackTraceLineLength < 1) { + L.w(`configToJson, Provided value for maxStackTraceLineLength is invalid!`) + } else { + json.maxStackTraceLineLength = config.sdkInternalLimits.maxStackTraceLineLength; + } + } + // Limits End -------------------------------------------- } catch (err) { L.e(`configToJson, Exception occured during converting config to json.${err.toString()}`); } diff --git a/android/build.gradle b/android/build.gradle index e87cd84e..54ccdcec 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -41,7 +41,7 @@ repositories { dependencies { implementation "com.facebook.react:react-native:${safeExtGet('reactNativeVersion', '+')}" - implementation 'ly.count.android:sdk:24.4.0' + implementation 'ly.count.android:sdk:24.4.1' // Import the BoM for the Firebase platform // The BoM version of 28.4.2 is the newest release that will target firebase-messaging version 22 diff --git a/android/src/main/java/ly/count/android/sdk/react/CountlyReactNative.java b/android/src/main/java/ly/count/android/sdk/react/CountlyReactNative.java index 434e5d3e..cf13ff23 100644 --- a/android/src/main/java/ly/count/android/sdk/react/CountlyReactNative.java +++ b/android/src/main/java/ly/count/android/sdk/react/CountlyReactNative.java @@ -89,7 +89,7 @@ public String toString() { public class CountlyReactNative extends ReactContextBaseJavaModule implements LifecycleEventListener { public static final String TAG = "CountlyRNPlugin"; - private String COUNTLY_RN_SDK_VERSION_STRING = "24.4.0"; + private String COUNTLY_RN_SDK_VERSION_STRING = "24.4.1"; private String COUNTLY_RN_SDK_NAME = "js-rnb-android"; private static final CountlyConfig config = new CountlyConfig(); @@ -235,6 +235,26 @@ private void populateConfig(JSONObject _config) { config.setRecordAppStartTime(_config.getBoolean("enableApm")); } // APM END -------------------------------------------- + // Limits ----------------------------------------------- + if(_config.has("maxKeyLength")) { + config.sdkInternalLimits.setMaxKeyLength(_config.getInt("maxKeyLength")); + } + if(_config.has("maxValueSize")) { + config.sdkInternalLimits.setMaxValueSize(_config.getInt("maxValueSize")); + } + if(_config.has("maxSegmentationValues")) { + config.sdkInternalLimits.setMaxSegmentationValues(_config.getInt("maxSegmentationValues")); + } + if(_config.has("maxBreadcrumbCount")) { + config.sdkInternalLimits.setMaxBreadcrumbCount(_config.getInt("maxBreadcrumbCount")); + } + if(_config.has("maxStackTraceLinesPerThread")) { + config.sdkInternalLimits.setMaxStackTraceLinesPerThread(_config.getInt("maxStackTraceLinesPerThread")); + } + if(_config.has("maxStackTraceLineLength")) { + config.sdkInternalLimits.setMaxStackTraceLineLength(_config.getInt("maxStackTraceLineLength")); + } + // Limits End ------------------------------------------- if (_config.has("crashReporting")) { config.enableCrashReporting(); } diff --git a/example/CountlyRNExample/Configuration.tsx b/example/CountlyRNExample/Configuration.tsx index b6ba1e64..38231af7 100644 --- a/example/CountlyRNExample/Configuration.tsx +++ b/example/CountlyRNExample/Configuration.tsx @@ -30,4 +30,13 @@ const countlyConfig = new CountlyConfig(COUNTLY_SERVER_KEY, COUNTLY_APP_KEY).set // .enableManualAppLoadedTrigger() // .setAppStartTimestampOverride(11223344); +// Countly SDK Limits ======================================== +// countlyConfig.limits +// .setMaxKeyLength() +// .setMaxValueSize() +// .setMaxSegmentationValues() +// .setMaxBreadcrumbCount() +// .setMaxStackTraceLineLength() +// .setMaxStackTraceLinesPerThread(); + export default countlyConfig; diff --git a/ios/src/CHANGELOG.md b/ios/src/CHANGELOG.md index 478a8faf..64bde998 100644 --- a/ios/src/CHANGELOG.md +++ b/ios/src/CHANGELOG.md @@ -1,3 +1,14 @@ +## 24.4.1 +* Added support for Feedback Widget terms and conditions + +* Mitigated an issue where SDK limits could affect internal keys +* Mitigated an issue that enabled recording reserved events +* Mitigated an issue where timed events could have no ID +* Mitigated an issue where internal limits were not being applied to some values +* Mitigated an issue where the request queue could overflow while sending a request + +* Removed timestamps from crash breadcrumbs + ## 24.4.0 * Added `attemptToSendStoredRequests` method to combine all events in event queue into a request and attempt to process stored requests * Added the iOS privacy manifest to the Countly SDK diff --git a/ios/src/Countly-PL.podspec b/ios/src/Countly-PL.podspec index 3f661a6a..e1590e8e 100644 --- a/ios/src/Countly-PL.podspec +++ b/ios/src/Countly-PL.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'Countly-PL' - s.version = '24.4.0' + s.version = '24.4.1' s.license = { :type => 'MIT', :file => 'LICENSE' } s.summary = 'Countly is an innovative, real-time, open source mobile analytics platform.' s.homepage = 'https://github.com/Countly/countly-sdk-ios' diff --git a/ios/src/Countly.m b/ios/src/Countly.m index f5f70233..01f6c2ee 100644 --- a/ios/src/Countly.m +++ b/ios/src/Countly.m @@ -153,7 +153,8 @@ - (void)startWithConfig:(CountlyConfig *)config CountlyCommon.sharedInstance.attributionID = config.attributionID; - CountlyDeviceInfo.sharedInstance.customMetrics = [config.customMetrics cly_truncated:@"Custom metric"]; + NSDictionary* customMetricsTruncated = [config.customMetrics cly_truncated:@"Custom metric"]; + CountlyDeviceInfo.sharedInstance.customMetrics = [customMetricsTruncated cly_limited:@"Custom metric"]; [Countly.user save]; @@ -206,7 +207,7 @@ - (void)startWithConfig:(CountlyConfig *)config #endif #endif - CountlyCrashReporter.sharedInstance.crashSegmentation = [config.crashSegmentation cly_truncated:@"Crash segmentation"]; + CountlyCrashReporter.sharedInstance.crashSegmentation = config.crashSegmentation; CountlyCrashReporter.sharedInstance.crashLogLimit = config.sdkInternalLimits.getMaxBreadcrumbCount; // For backward compatibility, deprecated values are only set incase new values are not provided using sdkInternalLimits interface if(CountlyCrashReporter.sharedInstance.crashLogLimit == kCountlyMaxBreadcrumbCount && config.crashLogLimit != kCountlyMaxBreadcrumbCount) { @@ -759,25 +760,22 @@ - (void)recordEvent:(NSString *)key segmentation:(NSDictionary *)segmentation co { CLY_LOG_I(@"%s %@ %@ %lu %f %f", __FUNCTION__, key, segmentation, (unsigned long)count, sum, duration); + if (!CountlyConsentManager.sharedInstance.consentForEvents) + { + CLY_LOG_W(@"Consent for events not given! Event will not be recorded."); + return; + } + BOOL isReservedEvent = [self isReservedEvent:key]; if (isReservedEvent) { - CLY_LOG_V(@"A reserved event detected: %@", key); - - if (!isReservedEvent) - { - CLY_LOG_W(@"Specific consent not given for the reserved event! So, it will not be recorded."); - return; - } - - CLY_LOG_V(@"Specific consent given for the reserved event! So, it will be recorded."); - } - else if (!CountlyConsentManager.sharedInstance.consentForEvents) - { - CLY_LOG_W(@"Events consent not given! Event will not be recorded."); + CLY_LOG_W(@"A reserved event detected for key: '%@', event will not be recorded.", key); return; } + + NSDictionary* truncated = [segmentation cly_truncated:@"Event segmentation"]; + segmentation = [truncated cly_limited:@"Event segmentation"]; [self recordEvent:key segmentation:segmentation count:count sum:sum duration:duration ID:nil timestamp:CountlyCommon.sharedInstance.uniqueTimestamp]; } @@ -807,7 +805,6 @@ - (void)recordEvent:(NSString *)key segmentation:(NSDictionary *)segmentation co return; CountlyEvent *event = CountlyEvent.new; - event.key = key; event.ID = ID; if (!event.ID.length) { @@ -829,10 +826,11 @@ - (void)recordEvent:(NSString *)key segmentation:(NSDictionary *)segmentation co // If the event is not reserved, assign the previous event ID to the current event's PEID property, or an empty string if previousEventID is nil. Then, update previousEventID to the current event's ID. if (!isReservedEvent) { + key = [key cly_truncatedKey:@"Event key"]; event.PEID = previousEventID ?: @""; previousEventID = event.ID; } - + event.key = key; event.segmentation = segmentation; event.count = MAX(count, 1); event.sum = sum; @@ -846,18 +844,17 @@ - (void)recordEvent:(NSString *)key segmentation:(NSDictionary *)segmentation co - (BOOL)isReservedEvent:(NSString *)key { - NSDictionary * reservedEvents = - @{ - kCountlyReservedEventOrientation: @(CountlyConsentManager.sharedInstance.consentForUserDetails), - kCountlyReservedEventStarRating: @(CountlyConsentManager.sharedInstance.consentForFeedback), - kCountlyReservedEventSurvey: @(CountlyConsentManager.sharedInstance.consentForFeedback), - kCountlyReservedEventNPS: @(CountlyConsentManager.sharedInstance.consentForFeedback), - kCountlyReservedEventPushAction: @(CountlyConsentManager.sharedInstance.consentForPushNotifications), - kCountlyReservedEventView: @(CountlyConsentManager.sharedInstance.consentForViewTracking), - }; + NSArray* reservedEvents = + @[ + kCountlyReservedEventOrientation, + kCountlyReservedEventStarRating, + kCountlyReservedEventSurvey, + kCountlyReservedEventNPS, + kCountlyReservedEventPushAction, + kCountlyReservedEventView, + ]; - NSNumber* aReservedEvent = reservedEvents[key]; - return aReservedEvent.boolValue; + return [reservedEvents containsObject:key]; } #pragma mark - @@ -872,8 +869,6 @@ - (void)startEvent:(NSString *)key CountlyEvent *event = CountlyEvent.new; event.key = key; event.timestamp = CountlyCommon.sharedInstance.uniqueTimestamp; - event.hourOfDay = CountlyCommon.sharedInstance.hourOfDay; - event.dayOfWeek = CountlyCommon.sharedInstance.dayOfWeek; [CountlyPersistency.sharedInstance recordTimedEvent:event]; } @@ -898,12 +893,8 @@ - (void)endEvent:(NSString *)key segmentation:(NSDictionary *)segmentation count return; } - event.segmentation = segmentation; - event.count = MAX(count, 1); - event.sum = sum; - event.duration = NSDate.date.timeIntervalSince1970 - event.timestamp; - - [CountlyPersistency.sharedInstance recordEvent:event]; + NSTimeInterval duration = NSDate.date.timeIntervalSince1970 - event.timestamp; + [self recordEvent:key segmentation:segmentation count:count sum:sum duration:duration]; } - (void)cancelEvent:(NSString *)key diff --git a/ios/src/Countly.podspec b/ios/src/Countly.podspec index 54e7f614..bd9efb1d 100644 --- a/ios/src/Countly.podspec +++ b/ios/src/Countly.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'Countly' - s.version = '24.4.0' + s.version = '24.4.1' s.license = { :type => 'MIT', :file => 'LICENSE' } s.summary = 'Countly is an innovative, real-time, open source mobile analytics platform.' s.homepage = 'https://github.com/Countly/countly-sdk-ios' diff --git a/ios/src/Countly.xcodeproj/project.pbxproj b/ios/src/Countly.xcodeproj/project.pbxproj index a76ecdf4..c7cddef1 100644 --- a/ios/src/Countly.xcodeproj/project.pbxproj +++ b/ios/src/Countly.xcodeproj/project.pbxproj @@ -58,6 +58,7 @@ 3B20A9D32245228700E3D7AE /* CountlyPushNotifications.h in Headers */ = {isa = PBXBuildFile; fileRef = 3B20A9AE2245228600E3D7AE /* CountlyPushNotifications.h */; }; 3B20A9D42245228700E3D7AE /* CountlyPersistency.m in Sources */ = {isa = PBXBuildFile; fileRef = 3B20A9AF2245228600E3D7AE /* CountlyPersistency.m */; }; 3B20A9D62245228700E3D7AE /* CountlyConsentManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 3B20A9B12245228700E3D7AE /* CountlyConsentManager.h */; }; + 968426812BF2302C007B303E /* CountlyConnectionManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 968426802BF2302C007B303E /* CountlyConnectionManagerTests.swift */; }; D219374B248AC71C00E5798B /* CountlyPerformanceMonitoring.h in Headers */ = {isa = PBXBuildFile; fileRef = D2193749248AC71C00E5798B /* CountlyPerformanceMonitoring.h */; }; D219374C248AC71C00E5798B /* CountlyPerformanceMonitoring.m in Sources */ = {isa = PBXBuildFile; fileRef = D219374A248AC71C00E5798B /* CountlyPerformanceMonitoring.m */; }; D249BF5E254D3D180058A6C2 /* CountlyFeedbackWidget.h in Headers */ = {isa = PBXBuildFile; fileRef = D249BF5C254D3D170058A6C2 /* CountlyFeedbackWidget.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -131,6 +132,7 @@ 3B20A9AE2245228600E3D7AE /* CountlyPushNotifications.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CountlyPushNotifications.h; sourceTree = ""; }; 3B20A9AF2245228600E3D7AE /* CountlyPersistency.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CountlyPersistency.m; sourceTree = ""; }; 3B20A9B12245228700E3D7AE /* CountlyConsentManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CountlyConsentManager.h; sourceTree = ""; }; + 968426802BF2302C007B303E /* CountlyConnectionManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyConnectionManagerTests.swift; sourceTree = ""; }; D2193749248AC71C00E5798B /* CountlyPerformanceMonitoring.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CountlyPerformanceMonitoring.h; sourceTree = ""; }; D219374A248AC71C00E5798B /* CountlyPerformanceMonitoring.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CountlyPerformanceMonitoring.m; sourceTree = ""; }; D249BF5C254D3D170058A6C2 /* CountlyFeedbackWidget.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CountlyFeedbackWidget.h; sourceTree = ""; }; @@ -164,6 +166,7 @@ 1A5C4C962B35B0850032EE1F /* CountlyTests.swift */, 1A50D7042B3C5AA3009C6938 /* CountlyBaseTestCase.swift */, 1AFD79012B3EF82C00772FBD /* CountlyTests-Bridging-Header.h */, + 968426802BF2302C007B303E /* CountlyConnectionManagerTests.swift */, ); path = CountlyTests; sourceTree = ""; @@ -333,7 +336,7 @@ attributes = { LastSwiftUpdateCheck = 1410; LastUpgradeCheck = 1410; - ORGANIZATIONNAME = "Alin Radut"; + ORGANIZATIONNAME = "Countly"; TargetAttributes = { 1A5C4C932B35B0850032EE1F = { CreatedOnToolsVersion = 14.1; @@ -387,6 +390,7 @@ files = ( 1A5C4C972B35B0850032EE1F /* CountlyTests.swift in Sources */, 1A50D7052B3C5AA3009C6938 /* CountlyBaseTestCase.swift in Sources */, + 968426812BF2302C007B303E /* CountlyConnectionManagerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -634,7 +638,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 24.4.0; + MARKETING_VERSION = 24.4.1; PRODUCT_BUNDLE_IDENTIFIER = ly.count.CountlyiOSSDK; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -666,7 +670,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 24.4.0; + MARKETING_VERSION = 24.4.1; PRODUCT_BUNDLE_IDENTIFIER = ly.count.CountlyiOSSDK; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/ios/src/Countly.xcodeproj/project.xcworkspace/xcuserdata/muhammadjunaidakram.xcuserdatad/UserInterfaceState.xcuserstate b/ios/src/Countly.xcodeproj/project.xcworkspace/xcuserdata/muhammadjunaidakram.xcuserdatad/UserInterfaceState.xcuserstate deleted file mode 100644 index 7038fd5e..00000000 Binary files a/ios/src/Countly.xcodeproj/project.xcworkspace/xcuserdata/muhammadjunaidakram.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ diff --git a/ios/src/Countly.xcodeproj/xcshareddata/xcbaselines/1A5C4C932B35B0850032EE1F.xcbaseline/454A8605-5E20-454E-97DD-739F4D09680E.plist b/ios/src/Countly.xcodeproj/xcshareddata/xcbaselines/1A5C4C932B35B0850032EE1F.xcbaseline/454A8605-5E20-454E-97DD-739F4D09680E.plist new file mode 100644 index 00000000..2031ab8e --- /dev/null +++ b/ios/src/Countly.xcodeproj/xcshareddata/xcbaselines/1A5C4C932B35B0850032EE1F.xcbaseline/454A8605-5E20-454E-97DD-739F4D09680E.plist @@ -0,0 +1,22 @@ + + + + + classNames + + CountlyTests + + testPerformanceExample() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.000054 + baselineIntegrationDisplayName + Local Baseline + + + + + + diff --git a/ios/src/Countly.xcodeproj/xcshareddata/xcbaselines/1A5C4C932B35B0850032EE1F.xcbaseline/Info.plist b/ios/src/Countly.xcodeproj/xcshareddata/xcbaselines/1A5C4C932B35B0850032EE1F.xcbaseline/Info.plist new file mode 100644 index 00000000..de772be7 --- /dev/null +++ b/ios/src/Countly.xcodeproj/xcshareddata/xcbaselines/1A5C4C932B35B0850032EE1F.xcbaseline/Info.plist @@ -0,0 +1,40 @@ + + + + + runDestinationsByUUID + + 454A8605-5E20-454E-97DD-739F4D09680E + + localComputer + + busSpeedInMHz + 0 + cpuCount + 1 + cpuKind + Apple M1 Pro + cpuSpeedInMHz + 0 + logicalCPUCoresPerPackage + 8 + modelCode + MacBookPro18,3 + physicalCPUCoresPerPackage + 8 + platformIdentifier + com.apple.platform.macosx + + targetArchitecture + arm64 + targetDevice + + modelCode + iPhone14,7 + platformIdentifier + com.apple.platform.iphonesimulator + + + + + diff --git a/ios/src/Countly.xcodeproj/xcuserdata/muhammadjunaidakram.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/ios/src/Countly.xcodeproj/xcuserdata/muhammadjunaidakram.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist deleted file mode 100644 index 29befaf8..00000000 --- a/ios/src/Countly.xcodeproj/xcuserdata/muhammadjunaidakram.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/ios/src/Countly.xcodeproj/xcuserdata/muhammadjunaidakram.xcuserdatad/xcschemes/xcschememanagement.plist b/ios/src/Countly.xcodeproj/xcuserdata/muhammadjunaidakram.xcuserdatad/xcschemes/xcschememanagement.plist deleted file mode 100644 index e53deb0a..00000000 --- a/ios/src/Countly.xcodeproj/xcuserdata/muhammadjunaidakram.xcuserdatad/xcschemes/xcschememanagement.plist +++ /dev/null @@ -1,19 +0,0 @@ - - - - - SchemeUserState - - Countly.xcscheme_^#shared#^_ - - orderHint - 0 - - CountlyTests.xcscheme_^#shared#^_ - - orderHint - 1 - - - - diff --git a/ios/src/CountlyCommon.h b/ios/src/CountlyCommon.h index 36a8562b..f5088e3a 100644 --- a/ios/src/CountlyCommon.h +++ b/ios/src/CountlyCommon.h @@ -117,7 +117,7 @@ void CountlyPrint(NSString *stringToPrint); #if (TARGET_OS_IOS) -@interface CLYInternalViewController : UIViewController +@interface CLYInternalViewController : UIViewController @property (nonatomic, weak) WKWebView* webView; @end diff --git a/ios/src/CountlyCommon.m b/ios/src/CountlyCommon.m index 75c9ddd2..f2405f25 100644 --- a/ios/src/CountlyCommon.m +++ b/ios/src/CountlyCommon.m @@ -26,7 +26,7 @@ @interface CountlyCommon () #endif @end -NSString* const kCountlySDKVersion = @"24.4.0"; +NSString* const kCountlySDKVersion = @"24.4.1"; NSString* const kCountlySDKName = @"objc-native-ios"; NSString* const kCountlyErrorDomain = @"ly.count.ErrorDomain"; @@ -36,6 +36,8 @@ @interface CountlyCommon () @implementation CountlyCommon +@synthesize lastTimestamp; + static CountlyCommon *s_sharedInstance = nil; static dispatch_once_t onceToken; + (instancetype)sharedInstance @@ -50,7 +52,8 @@ - (instancetype)init { gregorianCalendar = [NSCalendar.alloc initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; startTime = NSDate.date.timeIntervalSince1970; - + + self.lastTimestamp = 0; self.SDKVersion = kCountlySDKVersion; self.SDKName = kCountlySDKName; } @@ -328,11 +331,41 @@ - (void)viewWillLayoutSubviews #pragma GCC diagnostic pop } + self.webView.navigationDelegate = self; frame = UIEdgeInsetsInsetRect(frame, insets); self.webView.frame = frame; } } +- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { + NSString *url = navigationAction.request.URL.absoluteString; + if ([url containsString:@"cly_x_int=1"]) { + CLY_LOG_I(@"%s Opening url [%@] in external browser", __FUNCTION__, url); + [[UIApplication sharedApplication] openURL:navigationAction.request.URL options:@{} completionHandler:^(BOOL success) { + if (success) { + CLY_LOG_I(@"%s url [%@] opened in external browser", __FUNCTION__, url); + } + else { + CLY_LOG_I(@"%s unable to open url [%@] in external browser", __FUNCTION__, url); + } + }]; + decisionHandler(WKNavigationActionPolicyCancel); + return; + } + decisionHandler(WKNavigationActionPolicyAllow); +} + +- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation +{ + CLY_LOG_I(@"%s Web view has start loading", __FUNCTION__); + +} + +- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { + CLY_LOG_I(@"%s Web view has finished loading", __FUNCTION__); +} + + @end diff --git a/ios/src/CountlyConnectionManager.m b/ios/src/CountlyConnectionManager.m index e389c635..50ad12df 100644 --- a/ios/src/CountlyConnectionManager.m +++ b/ios/src/CountlyConnectionManager.m @@ -14,6 +14,8 @@ @interface CountlyConnectionManager () BOOL isSessionStarted; } @property (nonatomic) NSURLSession* URLSession; + +@property (nonatomic, strong) NSDate *startTime; @end NSString* const kCountlyQSKeyAppKey = @"app_key"; @@ -137,37 +139,47 @@ - (void)proceedOnQueue CLY_LOG_D(@"Proceeding on queue is aborted: SDK Networking is disabled from server config!"); return; } - + if (self.connection) { CLY_LOG_D(@"Proceeding on queue is aborted: Already has a request in process!"); return; } - + if (isCrashing) { CLY_LOG_D(@"Proceeding on queue is aborted: Application is crashing!"); return; } - + if (self.isTerminating) { CLY_LOG_D(@"Proceeding on queue is aborted: Application is terminating!"); return; } - + if (CountlyPersistency.sharedInstance.isQueueBeingModified) { CLY_LOG_D(@"Proceeding on queue is aborted: Queue is being modified!"); return; } + + if (!self.startTime) { + self.startTime = [NSDate date]; // Record start time only when it's not already recorded + CLY_LOG_D(@"Proceeding on queue started, queued request count %lu", [CountlyPersistency.sharedInstance remainingRequestCount]); + } NSString* firstItemInQueue = [CountlyPersistency.sharedInstance firstItemInQueue]; if (!firstItemInQueue) { - CLY_LOG_D(@"Queue is empty. All requests are processed."); + // Calculate total time when the queue becomes empty + NSTimeInterval elapsedTime = -[self.startTime timeIntervalSinceNow]; + CLY_LOG_D(@"Queue is empty. All requests are processed. Total time taken: %.2f seconds", elapsedTime); + // Reset start time for future queue processing + self.startTime = nil; return; } + BOOL isOldRequest = [CountlyPersistency.sharedInstance isOldRequest:firstItemInQueue]; if(isOldRequest) { @@ -276,6 +288,7 @@ - (void)proceedOnQueue else { CLY_LOG_D(@"%s, request:[ <%p> ] failed! response:[ %@ ]", __FUNCTION__, request, [data cly_stringUTF8]); + self.startTime = nil; } } else @@ -284,6 +297,7 @@ - (void)proceedOnQueue #if (TARGET_OS_WATCH) [CountlyPersistency.sharedInstance saveToFile]; #endif + self.startTime = nil; } }]; diff --git a/ios/src/CountlyCrashReporter.m b/ios/src/CountlyCrashReporter.m index f3c6e30c..23b56e63 100644 --- a/ios/src/CountlyCrashReporter.m +++ b/ios/src/CountlyCrashReporter.m @@ -218,7 +218,10 @@ - (void)recordException:(NSException *)exception isFatal:(BOOL)isFatal stackTrac { NSMutableDictionary* userInfo = [NSMutableDictionary dictionaryWithDictionary:exception.userInfo]; userInfo[kCountlyExceptionUserInfoBacktraceKey] = stackTrace; - userInfo[kCountlyExceptionUserInfoSegmentationOverrideKey] = segmentation; + if(segmentation) { + NSDictionary* truncatedSegmentation = [segmentation cly_truncated:@"Exception segmentation"]; + userInfo[kCountlyExceptionUserInfoSegmentationOverrideKey] = [truncatedSegmentation cly_limited:@"Exception segmentation"]; + } exception = [NSException exceptionWithName:exception.name reason:exception.reason userInfo:userInfo]; } @@ -350,21 +353,16 @@ - (void)log:(NSString *)log { if (!CountlyConsentManager.sharedInstance.consentForCrashReporting) return; - - const NSInteger kCountlyCustomCrashLogLengthLimit = 1000; - - if (log.length > kCountlyCustomCrashLogLengthLimit) - log = [log substringToIndex:kCountlyCustomCrashLogLengthLimit]; - - NSString* logWithDateTime = [NSString stringWithFormat:@"<%@> %@",[self.dateFormatter stringFromDate:NSDate.date], log]; + + log = [log cly_truncatedValue:@"Custom Crash log"]; if (self.shouldUsePLCrashReporter) { - [CountlyPersistency.sharedInstance writeCustomCrashLogToFile:logWithDateTime]; + [CountlyPersistency.sharedInstance writeCustomCrashLogToFile:log]; } else { - [self.customCrashLogs addObject:logWithDateTime]; + [self.customCrashLogs addObject:log]; if (self.customCrashLogs.count > self.crashLogLimit) [self.customCrashLogs removeObjectAtIndex:0]; @@ -478,6 +476,12 @@ - (BOOL)isMatchingFilter:(NSString *)string return YES; } +-(void) setCrashSegmentation:(NSDictionary*) crashSegmentation +{ + NSDictionary* truncatedSegmentation = [crashSegmentation cly_truncated:@"Crash segmentation"]; + _crashSegmentation = [truncatedSegmentation cly_limited:@"Crash segmentation"]; +} + @end diff --git a/ios/src/CountlyFeedbackWidget.m b/ios/src/CountlyFeedbackWidget.m index bb609a22..6dadbb3f 100644 --- a/ios/src/CountlyFeedbackWidget.m +++ b/ios/src/CountlyFeedbackWidget.m @@ -199,7 +199,22 @@ - (NSURLRequest *)displayRequest NSString* feedbackTypeEndpoint = [@"/" stringByAppendingString:self.type]; [URL appendString:feedbackTypeEndpoint]; [URL appendFormat:@"?%@", queryString]; + + // customParams is an NSDictionary containing the custom key-value pairs + NSDictionary *customParams = @{@"tc": @"1"}; + + // Build custom parameter string + NSMutableString *customString = [NSMutableString stringWithString:@"&custom="]; + [customString appendString:@"{"]; + [customParams enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + [customString appendFormat:@"%@:%@,", key, obj]; + }]; + [customString deleteCharactersInRange:NSMakeRange(customString.length - 1, 1)]; // Remove the last comma + [customString appendString:@"}"]; + // Append custom parameter + [URL appendString:customString]; + NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:URL]]; return request; } diff --git a/ios/src/CountlyPerformanceMonitoring.m b/ios/src/CountlyPerformanceMonitoring.m index 35fbbf26..3deb4c88 100644 --- a/ios/src/CountlyPerformanceMonitoring.m +++ b/ios/src/CountlyPerformanceMonitoring.m @@ -289,7 +289,8 @@ - (void)endCustomTrace:(NSString *)traceName metrics:(NSDictionary *)metrics } traceName = [traceName cly_truncatedKey:@"Custom trace name"]; - metrics = [metrics cly_truncated:@"Custom trace metric"]; + NSDictionary* metricsTruncated = [metrics cly_truncated:@"Custom trace metric"]; + metrics = [metricsTruncated cly_limited:@"Custom trace metric"]; NSNumber* endTime = @((long long)(CountlyCommon.sharedInstance.uniqueTimestamp * 1000)); diff --git a/ios/src/CountlyPersistency.m b/ios/src/CountlyPersistency.m index 3f64e365..d8bfa012 100644 --- a/ios/src/CountlyPersistency.m +++ b/ios/src/CountlyPersistency.m @@ -28,6 +28,8 @@ @implementation CountlyPersistency NSString* const kCountlyCustomCrashLogFileName = @"CountlyCustomCrash.log"; +NSUInteger const kCountlyRequestRemovalLoopLimit = 100; + static CountlyPersistency* s_sharedInstance = nil; static dispatch_once_t onceToken; @@ -86,15 +88,22 @@ - (void)addToQueue:(NSString *)queryString @synchronized (self) { - [self.queuedRequests addObject:queryString]; - if (self.queuedRequests.count > self.storedRequestsLimit && !CountlyConnectionManager.sharedInstance.connection) + if (self.queuedRequests.count >= self.storedRequestsLimit) { [self removeOldAgeRequestsFromQueue]; - if (self.queuedRequests.count > self.storedRequestsLimit && !CountlyConnectionManager.sharedInstance.connection) + if (self.queuedRequests.count >= self.storedRequestsLimit) { - [self.queuedRequests removeObjectAtIndex:0]; + NSUInteger exceededSize = self.queuedRequests.count - self.storedRequestsLimit; + // we should remove amount of limit at max + // for example if exceeded count is 136 and our limit is 100 we should remove 100 items + // in other case if exceeded count is 36 and out limit is 100 we can only remove 36 items because we have that amount + NSUInteger gonnaRemoveSize = MIN(exceededSize, kCountlyRequestRemovalLoopLimit) + 1; + CLY_LOG_W(@"[CountlyPersistency] addToQueue, request queue size:[ %lu ] exceeded limit:[ %lu ], will remove first:[ %lu ] request(s)", self.queuedRequests.count, self.storedRequestsLimit, gonnaRemoveSize); + NSRange itemsToRemove = NSMakeRange(0, gonnaRemoveSize); + [self.queuedRequests removeObjectsInRange:itemsToRemove]; } } + [self.queuedRequests addObject:queryString]; } } @@ -257,11 +266,6 @@ - (void)recordEvent:(CountlyEvent *)event { @synchronized (self.recordedEvents) { - event.key = [event.key cly_truncatedKey:@"Event key"]; - NSDictionary* truncated = [event.segmentation cly_truncated:@"Event segmentation"]; - NSDictionary* limited = [truncated cly_limited:@"Event segmentation"]; - event.segmentation = limited; - [self.recordedEvents addObject:event]; if (self.recordedEvents.count >= self.eventSendThreshold) diff --git a/ios/src/CountlyReactNative.m b/ios/src/CountlyReactNative.m index 62ce9978..a600a614 100644 --- a/ios/src/CountlyReactNative.m +++ b/ios/src/CountlyReactNative.m @@ -24,7 +24,7 @@ @interface CountlyFeedbackWidget () + (CountlyFeedbackWidget *)createWithDictionary:(NSDictionary *)dictionary; @end -NSString *const kCountlyReactNativeSDKVersion = @"24.4.0"; +NSString *const kCountlyReactNativeSDKVersion = @"24.4.1"; NSString *const kCountlyReactNativeSDKName = @"js-rnb-ios"; CLYPushTestMode const CLYPushTestModeProduction = @"CLYPushTestModeProduction"; @@ -147,7 +147,33 @@ - (void) populateConfig:(id) json { if (json[@"starRatingTextMessage"]) { config.starRatingMessage = json[@"starRatingTextMessage"]; } - + // Limits ----------------------------------------------- + // maxKeyLength + NSNumber *maxKeyLength = json[@"maxKeyLength"]; + if (maxKeyLength) { + [config.sdkInternalLimits setMaxKeyLength:[maxKeyLength intValue]]; + } + NSNumber *maxValueSize = json[@"maxValueSize"]; + if (maxValueSize) { + [config.sdkInternalLimits setMaxValueSize:[maxValueSize intValue]]; + } + NSNumber *maxSegmentationValues = json[@"maxSegmentationValues"]; + if (maxSegmentationValues) { + [config.sdkInternalLimits setMaxSegmentationValues:[maxSegmentationValues intValue]]; + } + NSNumber *maxBreadcrumbCount = json[@"maxBreadcrumbCount"]; + if (maxBreadcrumbCount) { + [config.sdkInternalLimits setMaxBreadcrumbCount:[maxBreadcrumbCount intValue]]; + } + NSNumber *maxStackTraceLineLength = json[@"maxStackTraceLineLength"]; + if (maxStackTraceLineLength) { + [config.sdkInternalLimits setMaxStackTraceLineLength:[maxStackTraceLineLength intValue]]; + } + NSNumber *maxStackTraceLinesPerThread = json[@"maxStackTraceLinesPerThread"]; + if (maxStackTraceLinesPerThread) { + [config.sdkInternalLimits setMaxStackTraceLinesPerThread:[maxStackTraceLinesPerThread intValue]]; + } + // Limits End ------------------------------------------- // APM ------------------------------------------------ NSNumber *enableForegroundBackground = json[@"enableForegroundBackground"]; if (enableForegroundBackground) { diff --git a/ios/src/CountlyTests/CountlyBaseTestCase.swift b/ios/src/CountlyTests/CountlyBaseTestCase.swift index fde220b4..12052126 100644 --- a/ios/src/CountlyTests/CountlyBaseTestCase.swift +++ b/ios/src/CountlyTests/CountlyBaseTestCase.swift @@ -3,7 +3,7 @@ // CountlyTests // // Created by Muhammad Junaid Akram on 27/12/2023. -// Copyright © 2023 Alin Radut. All rights reserved. +// Copyright © 2023 Countly. All rights reserved. // import XCTest diff --git a/ios/src/CountlyTests/CountlyConnectionManagerTests.swift b/ios/src/CountlyTests/CountlyConnectionManagerTests.swift new file mode 100644 index 00000000..e2812d0b --- /dev/null +++ b/ios/src/CountlyTests/CountlyConnectionManagerTests.swift @@ -0,0 +1,72 @@ +// +// CountlyConnectionManagerTests.swift +// CountlyTests +// +// Created by Arif Burak Demiray on 13.05.2024. +// Copyright © 2024 Countly. All rights reserved. +// + +import XCTest +@testable import Countly + +class CountlyConnectionManagerTests: CountlyBaseTestCase { + + /** + *
+     * 1- Init countly with the limit of 250 requests
+     *  - Check RQ is empty
+     * 2- Add 300 requests
+     *  - Check if the first 50 requests are removed
+     *  - Check size is 250
+     * 3- Stop the countly
+     * 4 - Init countly with the limit of 10 requests
+     *  - Check RQ is 250
+     * 5- Add 20 requests
+     *  - On every request addition queue should be dropped to the limit of 10
+     *  - On first one queue should be dropped to the 150
+     *  - On second one queue should be dropped to the 50
+     *  - On third one queue should be dropped to the 10
+     *  - On the last one queue should be size of 10
+     *  
+ */ + func test_addRequest_maxQueueSizeLimit_Scenario() throws { + let config = createBaseConfig() + config.storedRequestsLimit = 250 + config.manualSessionHandling = true + // No Device ID provided during init + Countly.sharedInstance().start(with: config) + + XCTAssertEqual(0, CountlyPersistency.sharedInstance().remainingRequestCount()) + + addRequests(count: 300) + XCTAssertEqual(250, CountlyPersistency.sharedInstance().remainingRequestCount()) + + Countly.sharedInstance().halt(false) + config.storedRequestsLimit = 10 + Countly.sharedInstance().start(with: config) + + XCTAssertEqual(250, CountlyPersistency.sharedInstance().remainingRequestCount()) + + addRequests(count: 1) + XCTAssertEqual(150, CountlyPersistency.sharedInstance().remainingRequestCount()) + + addRequests(count: 1) + XCTAssertEqual(50, CountlyPersistency.sharedInstance().remainingRequestCount()) + + addRequests(count: 1) + XCTAssertEqual(10, CountlyPersistency.sharedInstance().remainingRequestCount()) + + addRequests(count: 17) + XCTAssertEqual(10, CountlyPersistency.sharedInstance().remainingRequestCount()) + + Countly.sharedInstance().halt(true) + + } + + func addRequests(count: Int) { + for loop in 0...count-1 { + CountlyPersistency.sharedInstance().add(toQueue: "&request=REQUEST\(loop)") + } + CountlyPersistency.sharedInstance().saveToFileSync() + } +} diff --git a/ios/src/CountlyTests/CountlyTests.swift b/ios/src/CountlyTests/CountlyTests.swift index d1289132..54fc4168 100644 --- a/ios/src/CountlyTests/CountlyTests.swift +++ b/ios/src/CountlyTests/CountlyTests.swift @@ -3,7 +3,7 @@ // CountlyTests // // Created by Muhammad Junaid Akram on 22/12/2023. -// Copyright © 2023 Alin Radut. All rights reserved. +// Copyright © 2023 Countly. All rights reserved. // import XCTest diff --git a/ios/src/CountlyUserDetails.m b/ios/src/CountlyUserDetails.m index 9cd646f9..23414bd2 100644 --- a/ios/src/CountlyUserDetails.m +++ b/ios/src/CountlyUserDetails.m @@ -91,7 +91,10 @@ - (NSString *)serializedUserDetails userDictionary[kCountlyUDKeyBirthyear] = self.birthYear; if ([self.custom isKindOfClass:NSDictionary.class]) - self.custom = [((NSDictionary *)self.custom) cly_truncated:@"User details custom dictionary"]; + { + NSDictionary* customTruncated = [((NSDictionary *)self.custom) cly_truncated:@"User details custom dictionary"]; + self.custom = [customTruncated cly_limited:@"User details custom dictionary"]; + } if (self.custom) userDictionary[kCountlyUDKeyCustom] = self.custom; @@ -396,9 +399,54 @@ - (void)save [CountlyConnectionManager.sharedInstance sendUserDetails:[@{kCountlyLocalPicturePath: self.pictureLocalPath} cly_JSONify]]; if (self.modifications.count) - [CountlyConnectionManager.sharedInstance sendUserDetails:[@{kCountlyUDKeyCustom: self.modifications} cly_JSONify]]; + { + [CountlyConnectionManager.sharedInstance sendUserDetails:[@{kCountlyUDKeyCustom: [self truncateModifications]} cly_JSONify]]; + } [self clearUserDetails]; } + +- (NSDictionary *)truncateModifications +{ + NSMutableDictionary* truncatedDict = self.modifications.mutableCopy; + [self.modifications enumerateKeysAndObjectsUsingBlock:^(NSString * key, id obj, BOOL * stop) + { + NSString* truncatedKey = [key cly_truncatedKey:@"User details modifications key"]; + if (![truncatedKey isEqualToString:key]) + { + truncatedDict[truncatedKey] = obj; + [truncatedDict removeObjectForKey:key]; + } + + if ([obj isKindOfClass:NSString.class]) + { + NSString* truncatedValue = [obj cly_truncatedValue:@"User details modifications value"]; + if (![truncatedValue isEqualToString:obj]) + { + truncatedDict[truncatedKey] = truncatedValue; + } + } + else if ([obj isKindOfClass:NSDictionary.class]) + { + NSMutableDictionary* truncatedValueDict = ((NSDictionary *)obj).mutableCopy; + [(NSDictionary *)obj enumerateKeysAndObjectsUsingBlock:^(NSString * key, id value, BOOL * stop) + { + if ([value isKindOfClass:NSString.class]) + { + NSString* truncatedValue = [value cly_truncatedValue:@"User details modifications value"]; + if (![truncatedValue isEqualToString:value]) + { + truncatedValueDict[key] = truncatedValue; + truncatedDict[truncatedKey] = truncatedValueDict; + } + } + }]; + } + + }]; + + return truncatedDict.copy; +} + @end diff --git a/ios/src/CountlyViewTrackingInternal.m b/ios/src/CountlyViewTrackingInternal.m index e36ffcb1..1450bfbf 100644 --- a/ios/src/CountlyViewTrackingInternal.m +++ b/ios/src/CountlyViewTrackingInternal.m @@ -345,8 +345,6 @@ - (void)stopViewWithIDInternal:(NSString *) viewKey customSegmentation:(NSDictio if (viewData) { NSMutableDictionary* segmentation = NSMutableDictionary.new; - segmentation[kCountlyVTKeyName] = viewData.viewName; - segmentation[kCountlyVTKeySegment] = CountlyDeviceInfo.osName; if (viewData.segmentation) { @@ -365,6 +363,12 @@ - (void)stopViewWithIDInternal:(NSString *) viewKey customSegmentation:(NSDictio [segmentation addEntriesFromDictionary:mutableCustomSegmentation]; } + NSDictionary* segmentationTruncated = [segmentation cly_truncated:@"View segmentation"]; + segmentation = [segmentationTruncated cly_limited:@"View segmentation"].mutableCopy; + + segmentation[kCountlyVTKeyName] = viewData.viewName; + segmentation[kCountlyVTKeySegment] = CountlyDeviceInfo.osName; + NSTimeInterval duration = viewData.duration; [Countly.sharedInstance recordReservedEvent:kCountlyReservedEventView segmentation:segmentation count:1 sum:0 duration:duration ID:viewData.viewID timestamp:CountlyCommon.sharedInstance.uniqueTimestamp]; @@ -403,15 +407,6 @@ - (NSString*)startViewInternal:(NSString *)viewName customSegmentation:(NSDictio viewName = [viewName cly_truncatedKey:@"View name"]; NSMutableDictionary* segmentation = NSMutableDictionary.new; - segmentation[kCountlyVTKeyName] = viewName; - segmentation[kCountlyVTKeySegment] = CountlyDeviceInfo.osName; - segmentation[kCountlyVTKeyVisit] = @1; - - if (self.isFirstView) - { - self.isFirstView = NO; - segmentation[kCountlyVTKeyStart] = @1; - } if (self.viewSegmentation) { @@ -425,6 +420,18 @@ - (NSString*)startViewInternal:(NSString *)viewName customSegmentation:(NSDictio [segmentation addEntriesFromDictionary:mutableCustomSegmentation]; } + NSDictionary* segmentationTruncated = [segmentation cly_truncated:@"View segmentation"]; + segmentation = [segmentationTruncated cly_limited:@"View segmentation"].mutableCopy; + + segmentation[kCountlyVTKeyName] = viewName; + segmentation[kCountlyVTKeySegment] = CountlyDeviceInfo.osName; + segmentation[kCountlyVTKeyVisit] = @1; + + if (self.isFirstView) + { + self.isFirstView = NO; + segmentation[kCountlyVTKeyStart] = @1; + } self.previousViewID = self.currentViewID; self.currentViewID = CountlyCommon.sharedInstance.randomEventID; diff --git a/ios/src/Package.swift b/ios/src/Package.swift index 70ecfc6f..10bf0da8 100644 --- a/ios/src/Package.swift +++ b/ios/src/Package.swift @@ -36,6 +36,7 @@ let package = Package( "LICENSE", "README.md", "countly_dsym_uploader.sh", + "format.sh", "CHANGELOG.md", "SECURITY.md", "CountlyTests/" diff --git a/ios/src/format.sh b/ios/src/format.sh new file mode 100755 index 00000000..a3ee02f3 --- /dev/null +++ b/ios/src/format.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +CDP=$(osascript -e ' +tell application "Xcode" + activate + --wait for Xcode to remove edited flag from filename + delay 0.3 + set last_word_in_main_window to (word -1 of (get name of window 1)) + set current_document to document 1 whose name ends with last_word_in_main_window + set current_document_path to path of current_document + --CDP is assigned last set value: current_document_path +end tell ') + +sleep 0.6 ### during save Xcode stops listening for file changes +/usr/local/bin/clang-format -style=file -i ${CDP} + +# EOF diff --git a/lib/configuration_interfaces/countly_config_limits.js b/lib/configuration_interfaces/countly_config_limits.js new file mode 100644 index 00000000..ed410a86 --- /dev/null +++ b/lib/configuration_interfaces/countly_config_limits.js @@ -0,0 +1,89 @@ +/** + * Countly SDK React Native Bridge SDK Internal Limits + * https://github.com/Countly/countly-sdk-react-native-bridge + * @Countly + */ + +// This class holds SDK internal limits (https://support.count.ly/hc/en-us/articles/360037753291-SDK-development-guide#01H821RTQ7AZ6J858BHP4883ZC) specific configurations to be used with CountlyConfig class and serves as an interface. +// You can chain multiple configurations. +class CountlyConfigSDKInternalLimits { + constructor() { + _maxKeyLength = 0; + _maxValueSize = 0; + _maxSegmentationValues = 0; + _maxBreadcrumbCount = 0; + _maxStackTraceLinesPerThread = 0; + _maxStackTraceLineLength = 0; + } + + // getters + get maxKeyLength() { + return this._maxKeyLength; + } + + get maxValueSize() { + return this._maxValueSize; + } + + get maxSegmentationValues() { + return this._maxSegmentationValues; + } + + get maxBreadcrumbCount() { + return this._maxBreadcrumbCount; + } + + get maxStackTraceLinesPerThread() { + return this._maxStackTraceLinesPerThread; + } + + get maxStackTraceLineLength() { + return this._maxStackTraceLineLength; + } + + // setters / methods + + // Limits the maximum size of all string keys + // keyLengthLimit is the maximum char size of all string keys (default 128 chars) + setMaxKeyLength(keyLengthLimit) { + this._maxKeyLength = keyLengthLimit; + return this; + } + + // Limits the size of all values in segmentation key-value pairs + // valueSizeLimit is the maximum char size of all values in our key-value pairs (default 256 chars) + setMaxValueSize(valueSizeLimit) { + this._maxValueSize = valueSizeLimit; + return this; + } + + // Limits the max amount of custom segmentation in one event + // segmentationAmountLimit is the max amount of custom segmentation in one event (default 100 key-value pairs) + setMaxSegmentationValues(segmentationAmountLimit) { + this._maxSegmentationValues = segmentationAmountLimit; + return this; + } + + // Limits the max amount of breadcrumbs that can be recorded before the oldest one is deleted + // breadcrumbCountLimit is the max amount of breadcrumbs that can be recorded before the oldest one is deleted (default 100) + setMaxBreadcrumbCount(breadcrumbCountLimit) { + this._maxBreadcrumbCount = breadcrumbCountLimit; + return this; + } + + // Limits the max amount of stack trace lines to be recorded per thread + // stackTraceLinesPerThreadLimit is the max amount of stack trace lines to be recorded per thread (default 30) + setMaxStackTraceLinesPerThread(stackTraceLinesPerThreadLimit) { + this._maxStackTraceLinesPerThread = stackTraceLinesPerThreadLimit; + return this; + } + + // Limits the max characters allowed per stack trace lines. Also limits the crash message length + // stackTraceLineLengthLimit is the max length of each stack trace line (default 200) + setMaxStackTraceLineLength(stackTraceLineLengthLimit) { + this._maxStackTraceLineLength = stackTraceLineLengthLimit; + return this; + } +} + +export default CountlyConfigSDKInternalLimits; \ No newline at end of file diff --git a/package.json b/package.json index 1a613167..a0469c9c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "countly-sdk-react-native-bridge", - "version": "24.4.0", + "version": "24.4.1", "author": "Countly (https://count.ly/)", "bugs": { "url": "https://github.com/Countly/countly-sdk-react-native-bridge/issues"