From 3bfa163c7e8c81ca63e5b6b9ac0f2de609c50dff Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Fri, 23 Oct 2026 10:52:29 +0300 Subject: [PATCH] feat: add setMaxKeyLength init config option Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + .../main/java/ly/count/sdk/java/Config.java | 25 +++++++ .../sdk/java/internal/InternalConfig.java | 9 +++ .../java/internal/ModuleConfiguration.java | 1 + .../sdk/java/internal/ConfigSurfaceTests.java | 11 +++ .../ModuleConfigurationLimitsTests.java | 71 +++++++++++++++++++ 6 files changed, 118 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5e1a5a5..a24cb8d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ * Added "disableBackoffMechanism()" init config method to disable the backoff behavior. * Added support for server requested SDK log gathering. When the server asks for it, the SDK uploads its own internal log lines, even if logging is disabled. While consent is required, a line is uploaded only while the consent of the feature it belongs to is given. * Added support for the SDK connection test. When the server asks for it, the SDK checks that it can reach every endpoint it depends on and reports the results. +* Added "setMaxKeyLength(int)" init config method to limit the length of event keys, view names, segmentation keys and custom user property keys. Longer keys are truncated. There is no limit by default, and the key length limit of the SDK behavior settings overrides it. * Mitigated an issue where saving the SDK storage while remote config values were changing could empty the storage file. * Mitigated an issue where a storage file cut short by a stopped process made initialization fail. diff --git a/sdk-java/src/main/java/ly/count/sdk/java/Config.java b/sdk-java/src/main/java/ly/count/sdk/java/Config.java index 5cf1f4c2..732514be 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/Config.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/Config.java @@ -226,6 +226,11 @@ public class Config { */ protected int maxBreadcrumbCount = 100; + /** + * Maximum length of the keys the SDK records, no limit by default. + */ + protected int maxKeyLength = Integer.MAX_VALUE; + /** * Enable automatic crash reporting for unhandled exceptions. */ @@ -322,6 +327,26 @@ public Config setMaxBreadcrumbCount(int maxBreadcrumbCount) { return this; } + /** + * Set the maximum length of the keys the SDK records: event keys, view names, segmentation keys, + * custom user property keys, and the keys of custom user property modifiers. Longer keys are + * truncated to this length. There is no limit by default. The key length limit of the SDK behavior + * settings overrides this value. + * + * @param maxKeyLength the maximum key length, at least 1 + * @return {@code this} instance for method chaining + */ + public Config setMaxKeyLength(int maxKeyLength) { + if (maxKeyLength < 1) { + if (configLog != null) { + configLog.e("[Config] maxKeyLength cannot be less than 1"); + } + } else { + this.maxKeyLength = maxKeyLength; + } + return this; + } + /** * Disable automatic crash reporting for unhandled exceptions. * diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/InternalConfig.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/InternalConfig.java index 8d633c1d..4fb0c8e1 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/InternalConfig.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/InternalConfig.java @@ -245,6 +245,15 @@ int getMaxBreadcrumbCount() { return maxBreadcrumbCount; } + /** + * The key length limit the developer set. + * + * @return the limit, {@link Integer#MAX_VALUE} when none is set + */ + int getMaxKeyLength() { + return maxKeyLength; + } + String[] getLocationParams() { return new String[] { country, city, location, ip }; } diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleConfiguration.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleConfiguration.java index 58f6ba70..1f3d881f 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleConfiguration.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleConfiguration.java @@ -290,6 +290,7 @@ private void seedFromDeveloperConfig(@Nonnull InternalConfig config) { currentVEventQueueSizeThreshold = config.getEventsBufferSize(); currentVSessionUpdateInterval = config.getSendUpdateEachSeconds(); currentVMaxBreadcrumbCount = config.getMaxBreadcrumbCount(); + currentVMaxKeyLength = config.getMaxKeyLength(); currentVZoneTimerInterval = config.content.zoneTimerInterval; } diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/ConfigSurfaceTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/ConfigSurfaceTests.java index 25086e94..fb4db1d1 100644 --- a/sdk-java/src/test/java/ly/count/sdk/java/internal/ConfigSurfaceTests.java +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/ConfigSurfaceTests.java @@ -51,6 +51,7 @@ public void fluentChain_acceptsEverySaneValueAndReadsItBack() { .setEventQueueSizeToSend(9) .setRequestQueueMaxSize(321) .setMaxBreadcrumbCount(12) + .setMaxKeyLength(64) .setNetworkConnectTimeout(11) .setNetworkReadTimeout(12) .setNetworkRequestCooldown(1300) @@ -92,6 +93,7 @@ public void fluentChain_acceptsEverySaneValueAndReadsItBack() { Assert.assertEquals("test-platform", SDKCore.instance.config.getSdkPlatform()); // These two are only readable off the internal config, which is what the modules see. Assert.assertEquals(12, SDKCore.instance.config.getMaxBreadcrumbCount()); + Assert.assertEquals(64, SDKCore.instance.config.getMaxKeyLength()); Assert.assertFalse(SDKCore.instance.config.isUnhandledCrashReportingEnabled()); } @@ -144,6 +146,15 @@ public void outOfRangeValues_areRefusedAndLeaveTheGoodValueInPlace() { config.setEventQueueSizeToSend(-5); Assert.assertEquals("a negative event queue size must be refused", 23, config.getEventsBufferSize()); + // key length accepts 1 and up, and sets no limit until it is called + Assert.assertEquals(Integer.MAX_VALUE, new InternalConfig(config).getMaxKeyLength()); + config.setMaxKeyLength(40); + for (int bad : new int[] { 0, -1, Integer.MIN_VALUE }) { + config.setMaxKeyLength(bad); + Assert.assertEquals("key length must reject " + bad, 40, new InternalConfig(config).getMaxKeyLength()); + } + Assert.assertEquals(1, new InternalConfig(config.setMaxKeyLength(1)).getMaxKeyLength()); + // The boundary values themselves are valid. Assert.assertEquals(300, config.setNetworkConnectTimeout(300).getNetworkConnectionTimeout()); Assert.assertEquals(1, config.setNetworkReadTimeout(1).getNetworkReadTimeout()); diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleConfigurationLimitsTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleConfigurationLimitsTests.java index e718ee49..88853b8b 100644 --- a/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleConfigurationLimitsTests.java +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleConfigurationLimitsTests.java @@ -356,6 +356,77 @@ public void aResponseChangingTheLimits_appliesFromTheNextCall() { Assert.assertEquals(truncateLines(stackTraceOf(exception), 12), crashes.get(0).getString("_error")); } + /** + * The key length limit of the developer configuration cuts keys while the settings set none, the + * settings override it while they carry {@code lkl}, and it applies again once they stop. + *

+ * Verifies that a developer limit of 5 cuts the event key and the segmentation keys while the + * values stay whole, that a response with a key length limit of 8 cuts to 8 instead, and that a + * response leaving the limit out brings back the developer limit of 5. + */ + @Test + public void theDeveloperKeyLengthLimit_cutsKeys_untilTheSettingsOverrideIt() { + server.respondWith(new ServerConfigBuilder().networking(false)); + init(configWithWarnings().enableFeatures(Config.Feature.Events).setEventQueueSizeToSend(100).setMaxKeyLength(5)); + Assert.assertEquals(5, provider().getMaxKeyLength()); + Map segmentation = TestUtils.map("segment_key", "segment_value", "second", 2); + + Countly.instance().events().recordEvent("event_key", segmentation); + push(new ServerConfigBuilder().keyLengthLimit(8)); + Assert.assertEquals(8, provider().getMaxKeyLength()); + Countly.instance().events().recordEvent("event_key", segmentation); + push(new ServerConfigBuilder().networking(false)); + Assert.assertEquals(5, provider().getMaxKeyLength()); + Countly.instance().events().recordEvent("event_key", segmentation); + + List queued = TestUtils.getCurrentEQ(); + Assert.assertEquals(keysOf(queued).toString(), 3, queued.size()); + assertEvent(queued.get(0), "event", TestUtils.map("segme", "segment_value", "secon", 2)); + assertEvent(queued.get(1), "event_ke", TestUtils.map("segment_", "segment_value", "second", 2)); + assertEvent(queued.get(2), "event", TestUtils.map("segme", "segment_value", "secon", 2)); + } + + /** + * Without any SDK behavior settings, the key length limit of the developer configuration cuts the + * keys of events, views and custom user properties. + *

+ * Verifies with a limit of 4 the event key and segmentation keys, the view name, and the custom + * user property keys of a set and of a modification, while the keys the SDK adds to a view stay + * whole. + */ + @Test + public void withoutSettings_theDeveloperKeyLengthLimitCutsKeys() { + Countly.instance().init(TestUtils.getBaseConfig().setLogListener(this::keepWarning) + .enableFeatures(Config.Feature.Events, Config.Feature.Views, Config.Feature.UserProfiles) + .setEventQueueSizeToSend(100).setMaxKeyLength(4)); + + Countly.instance().events().recordEvent("event_key", TestUtils.map("segment_key", "segment_value")); + Countly.instance().views().startView("view_name"); + List queued = TestUtils.getCurrentEQ(); + Assert.assertEquals(keysOf(queued).toString(), 2, queued.size()); + assertEvent(queued.get(0), "even", TestUtils.map("segm", "segment_value")); + Assert.assertEquals("view", queued.get(1).segmentation.get(ModuleViews.KEY_NAME)); + Assert.assertEquals("1", queued.get(1).segmentation.get(ModuleViews.KEY_VISIT)); + + Countly.instance().userProfile().setProperty("custom_key", "custom_value"); + Countly.instance().userProfile().push("list_key", "item"); + Countly.instance().userProfile().save(); + TestUtils.letTheClockCatchUp(); + + JSONObject details = null; + for (Map request : TestUtils.getCurrentRQ()) { + if (request.containsKey("user_details")) { + details = new JSONObject(request.get("user_details")); + } + } + Assert.assertNotNull(details); + JSONObject custom = details.getJSONObject(ModuleUserProfile.CUSTOM_KEY); + Assert.assertEquals("custom_value", custom.getString("cust")); + Assert.assertEquals("item", custom.getJSONObject("list").getString("$push")); + Assert.assertEquals(2, custom.length()); + Assert.assertTrue(warningsContaining("key length limit of [4]") > 0); + } + /** * With a live response that sets no limit, nothing any feature sends is cut, however long or wide. *