Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
f0be144
feat: SBS
arifBurakDemiray Jul 14, 2026
42e85b8
Merge branch 'staging' into sdk-behavior-settings
arifBurakDemiray Sep 30, 2026
49c7e03
feat: sbs all, log gathering, connection test
arifBurakDemiray Sep 30, 2026
b61ef4f
Keep the SDK starting over a store file cut short
arifBurakDemiray Oct 21, 2026
17cb726
Replace the stored SDK behavior settings with each answer
arifBurakDemiray Oct 21, 2026
f26cb58
feat: more settings
arifBurakDemiray Oct 22, 2026
a6fe4ba
Merge pull request #307 from Countly/sbs-review-fixes
arifBurakDemiray Oct 22, 2026
4bf5d65
Merge branch 'staging' into sdk-behavior-settings
arifBurakDemiray Oct 22, 2026
5e9b244
Gather log lines under their feature's consent, one queued batch at a…
arifBurakDemiray Oct 22, 2026
a9529bb
Hand out feature interfaces that are never null, stop only the remove…
arifBurakDemiray Oct 22, 2026
15d7186
Keep breadcrumbs during a crash, the logger on a refused init, quiet …
arifBurakDemiray Oct 22, 2026
7e50a43
Merge pull request #319 from Countly/p7-log-gathering-fixes
arifBurakDemiray Oct 22, 2026
45315a0
Merge pull request #320 from Countly/sbs-consent-and-facades
arifBurakDemiray Oct 22, 2026
0e5896e
Merge pull request #321 from Countly/sbs-small-core-fixes
arifBurakDemiray Oct 22, 2026
6093958
Wait for the lines logged after a request's removal before asserting …
arifBurakDemiray Oct 22, 2026
18d5076
Merge pull request #322 from Countly/sbs-ci-test-race
arifBurakDemiray Oct 22, 2026
66a11ce
Address the Codacy findings worth changing
arifBurakDemiray Oct 22, 2026
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
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,40 @@
</repositories>
```
Versions released before this one stay available on Maven Central. The files are signed with the same key as before (`FC8DB0DE234A273BA45E562FB8C83A079A5BBD0C`).
* ! Minor breaking change ! SDK behavior settings are now enabled by default. The SDK fetches them from the server at initialization and every few hours, so changes made in SDK Manager > SDK Configuration on your server affect SDK behavior directly. They are not used in backend mode.
* ! Minor breaking change ! The feature interfaces of "Countly.instance()" ("events()", "views()", "crashes()", "location()", "userProfile()", "remoteConfig()", "feedback()", "content()" and "deviceId()") no longer return null.
Before initialization, while the consent of a feature is not given, or when a feature is not enabled, they return an interface that ignores calls until the feature runs and passes an error to the callbacks of the calls it ignores.
An interface you keep sends its calls to the feature once it runs. Use "Countly.isTracking" to check whether a feature runs.

* Added support for SDK behavior settings that let the server control the SDK:
* Switches for tracking, networking, sessions, views, custom events, crash reporting, automatic crash reporting, location, and console logging
* The event queue size, request queue size, session update interval, and settings refresh interval
* The consent requirement, applied on the next initialization
* SDK internal limits for key length, value size, segmentation value count, stack trace lines per thread, stack trace line length, and breadcrumb count
* Listing filters (blacklist/whitelist) for events, segmentation keys, event specific segmentation keys, and user properties, plus a user property cache limit
* Entering, refreshing, and the fetch interval of the content zone, plus Journey Trigger Events and Journey Trigger Views that refresh it
* Dropping requests older than a given age
* Added "setSdkBehaviorSettings(String)" init config method for providing SDK behavior settings at initialization.
* Added "disableSdkBehaviorSettingsUpdates()" init config method for disabling SDK behavior settings requests.
* Improved request queue handling with a built-in backoff mechanism which is enabled by default. When the server takes 10 seconds or longer to answer while few requests are queued, the SDK pauses sending for 60 seconds.
* 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.

* 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.
* Mitigated an issue where a request recorded while the SDK was finishing a send could wait until the next request or session update before it was sent.

* Updated JSON library version from "20250517" to "20260814".

* Mitigated an issue where requests, sessions or crashes created close together could get the same storage ID, so some of them were lost.
* Mitigated an issue where removing the consent of one feature also stopped the other features and deleted their queued events, unsent crashes and remote config values, after which the device ID and user profile interfaces could throw.
* Mitigated an issue where, while consent was required, unhandled crashes were not recorded if the crash reporting consent was given after initialization.
* Mitigated an issue where removing the location consent did not erase the location on the server.
* Mitigated an issue where an error in one feature during a timer tick stopped the SDK timer, and with it session updates and event sending.
* Mitigated an issue where verbose logs showed the values of custom network request headers. Only the number of headers is logged now.
* Mitigated an issue where a fresh initialization logged an error for storage files that did not exist yet.
* Mitigated an issue where a crash breadcrumb added while a crash was being recorded on another thread could be lost.

## 26.8.0
* Added support for the Content feature, accessible through the "Countly.instance().content()" interface:
Expand Down
62 changes: 62 additions & 0 deletions sdk-java/src/main/java/ly/count/sdk/java/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,12 @@
protected int networkReadTimeout = 30;

/**
* How long to wait between requests in milliseconds. Used to decrease CPU & I/O load on the device.

Check warning on line 107 in sdk-java/src/main/java/ly/count/sdk/java/Config.java

View workflow job for this annotation

GitHub Actions / Release checks (26.8.0)

invalid input: '&'
*/
protected int networkRequestCooldown = 1000;

/**
* How long to wait between Device ID change & push token requests, in milliseconds.

Check warning on line 112 in sdk-java/src/main/java/ly/count/sdk/java/Config.java

View workflow job for this annotation

GitHub Actions / Release checks (26.8.0)

invalid input: '&'

Check warning on line 112 in sdk-java/src/main/java/ly/count/sdk/java/Config.java

View workflow job for this annotation

GitHub Actions / Release checks (26.8.0)

invalid input: '&'
* Required by Countly server, don't change unless you know what you're doing!
*/
protected int networkImportantRequestCooldown = 5000;
Expand All @@ -126,7 +126,7 @@
* This ensures pinning continues to work after certificate prolongation.
* Certificates ({@link #certificatePins}) on the other hand have specific expiry date.
* In case you chose this way of pinning, you MUST ensure that ALL installs of your app
* have both certificates (old & new) until expiry date.

Check warning on line 129 in sdk-java/src/main/java/ly/count/sdk/java/Config.java

View workflow job for this annotation

GitHub Actions / Release checks (26.8.0)

invalid input: '&'
*/
protected Set<String> publicKeyPins = null;

Expand All @@ -142,7 +142,7 @@
* This ensures pinning continues to work after certificate prolongation.
* Certificates on the other hand have specific expiry date.
* In case you chose this way of pinning, you MUST ensure that ALL installs of your app
* have both certificates (old & new) until expiry date.

Check warning on line 145 in sdk-java/src/main/java/ly/count/sdk/java/Config.java

View workflow job for this annotation

GitHub Actions / Release checks (26.8.0)

invalid input: '&'
*/
protected Set<String> certificatePins = null;

Expand All @@ -168,7 +168,7 @@
/**
* {@link CrashProcessor}-implementing class which is instantiated when application
* crashes or crash is reported programmatically using {@link Session#addCrashReport(Throwable, boolean, String, Map, String...)}.
* Crash processor helps you to add custom data in event of a crash: custom crash segments & crash logs.

Check warning on line 171 in sdk-java/src/main/java/ly/count/sdk/java/Config.java

View workflow job for this annotation

GitHub Actions / Release checks (26.8.0)

invalid input: '&'
*/
protected String crashProcessorClass = null;

Expand Down Expand Up @@ -253,6 +253,26 @@
protected boolean locationEnabled = true;
protected boolean autoSendUserProperties = true;

/**
* Seed value for the SDK behavior settings. Used on first init only,
* when no settings have yet been persisted; subsequent inits use the
* value pulled from storage.
*/
protected String sdkBehaviorSettings = null;

/**
* If {@code true}, the SDK skips the periodic {@code /o/sdk?method=sc}
* fetch. The seed JSON from {@link #setSdkBehaviorSettings(String)} (or
* a previously-persisted snapshot) is still applied.
*/
protected boolean sdkBehaviorSettingsRequestsDisabled = false;

/**
* Whether the backoff mechanism may hold the request queue back after the server answered
* slowly. The SDK behavior settings can override it.
*/
protected boolean backoffMechanismEnabled = true;

// TODO: storage limits & configuration
// protected int maxRequestsStored = 0;
// protected int storageDirectory = "";
Expand Down Expand Up @@ -668,7 +688,7 @@
}

/**
* Disable update requests completely. Only begin & end requests will be sent + some special

Check warning on line 691 in sdk-java/src/main/java/ly/count/sdk/java/Config.java

View workflow job for this annotation

GitHub Actions / Release checks (26.8.0)

invalid input: '&'
* cases if applicable like User Profile change or Push token updated.
*
* @return {@code this} instance for method chaining
Expand Down Expand Up @@ -766,7 +786,7 @@

/**
* How long to wait between requests in seconds.
* Used to decrease CPU & I/O load on the device in case of batch requests.

Check warning on line 789 in sdk-java/src/main/java/ly/count/sdk/java/Config.java

View workflow job for this annotation

GitHub Actions / Release checks (26.8.0)

invalid input: '&'
*
* @param milliseconds cooldown period in seconds
* @return {@code this} instance for method chaining
Expand All @@ -784,7 +804,7 @@

/**
* Set read timeout in seconds for HTTP requests SDK sends to Countly server. Defaults to 30.
* Used to decrease CPU & I/O load on the device in case of batch requests.

Check warning on line 807 in sdk-java/src/main/java/ly/count/sdk/java/Config.java

View workflow job for this annotation

GitHub Actions / Release checks (26.8.0)

invalid input: '&'
*
* @param milliseconds read timeout in milliseconds
* @return {@code this} instance for method chaining
Expand Down Expand Up @@ -812,7 +832,7 @@
* This ensures pinning continues to work after certificate prolongation.
* Certificates ({@link #certificatePins}) on the other hand have specific expiry date.
* In case you chose this way of pinning, you MUST ensure that ALL installs of your app
* have both certificates (old & new) until expiry date.

Check warning on line 835 in sdk-java/src/main/java/ly/count/sdk/java/Config.java

View workflow job for this annotation

GitHub Actions / Release checks (26.8.0)

invalid input: '&'
*
* NOTE: when {@link #serverURL} doesn't have {@code "https://"} public key pinning doesn't work
*
Expand Down Expand Up @@ -1508,4 +1528,46 @@
this.autoSendUserProperties = false;
return this;
}

/**
* Provide an initial SDK behavior settings payload to apply at init time,
* before the first {@code /o/sdk?method=sc} fetch returns. The value is
* the same JSON shape the Countly server emits: {@code {"v":1,"t":<ts>,
* "c":{...}}}. The seed is only used when nothing has been persisted yet,
* and it stands in for the server's settings: the first answer from the
* server replaces it, so a setting the answer does not carry goes back to
* this configuration. A {@code "cr":true} here can be lifted by that answer;
* use {@link #setRequiresConsent(boolean)} for a requirement that must hold.
*
* @param sdkBehaviorSettings serialized config JSON, or {@code null} for none
* @return {@code this} instance for method chaining
*/
public Config setSdkBehaviorSettings(String sdkBehaviorSettings) {
this.sdkBehaviorSettings = sdkBehaviorSettings;
return this;
}

/**
* Disable the periodic SDK behavior settings fetch from the server. Any
* seed JSON or previously-persisted snapshot is still applied at init.
*
* @return {@code this} instance for method chaining
*/
public Config disableSdkBehaviorSettingsUpdates() {
this.sdkBehaviorSettingsRequestsDisabled = true;
return this;
}

/**
* Disable the backoff mechanism. While it is on, a request the server takes long to answer
* makes the SDK wait before it sends the next queued request, which gives a busy server room
* to recover. It never waits while many requests are queued or after an old request. The SDK
* behavior settings can still turn it on or off.
*
* @return {@code this} instance for method chaining
*/
public Config disableBackoffMechanism() {
this.backoffMechanismEnabled = false;
return this;
}
}
59 changes: 34 additions & 25 deletions sdk-java/src/main/java/ly/count/sdk/java/Countly.java
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ public void init(final Config config) {
}

Log L = new Log(internalConfig.loggingLevel, internalConfig.logListener);
internalConfig.setLogger(L);

if (directory == null) {
L.e("[Countly] File cannot be null");
Expand All @@ -104,6 +103,7 @@ public void init(final Config config) {
L.e("[Countly] Countly shouldn't be initialized twice. Please either use Countly.isInitialized() to check status or call Countly.stop() before second Countly.init(). Calling 'init' the second time will now do nothing");
return;
}
internalConfig.setLogger(L);

if (internalConfig.enableBackendMode) {
internalConfig.sdkName = "java-native-backend";
Expand Down Expand Up @@ -396,14 +396,15 @@ public static void onConsentRemoval(Config.Feature... features) {
* <code>Feedback</code> interface to use feedback widgets nps,rating and forms.
* And do manual reporting of feedbacks.
*
* @return {@link ModuleFeedback.Feedback} instance.
* @return {@link ModuleFeedback.Feedback} instance, never {@code null}: before init, or while the feature does not
* run, as its consent is not given or it is not enabled, one that ignores calls until it runs
*/
public ModuleFeedback.Feedback feedback() {
if (!isInitialized()) {
if (L != null) {
L.e("[Countly] SDK is not initialized yet.");
}
return null;
return SDKCore.dormantFeedback;
}
return sdk.feedback();
}
Expand All @@ -413,45 +414,48 @@ public ModuleFeedback.Feedback feedback() {
* content block. A {@link ly.count.sdk.java.internal.ContentDisplay} has to be registered
* before a content zone can be entered; the "ly.count.sdk:java-ui" artifact ships a JavaFX one.
*
* @return {@link ModuleContent.Content} instance.
* @return {@link ModuleContent.Content} instance, never {@code null}: before init, or while the feature does not
* run, as its consent is not given or it is not enabled, one that ignores calls until it runs
* @apiNote This is an EXPERIMENTAL feature, and it can have breaking changes
*/
public ModuleContent.Content content() {
if (!isInitialized()) {
if (L != null) {
L.e("[Countly] content, SDK is not initialized yet.");
}
return null;
return SDKCore.dormantContent;
}
return sdk.content();
}

/**
* <code>RemoteConfig</code> interface to use remote config feature.
*
* @return {@link ModuleRemoteConfig.RemoteConfig} instance.
* @return {@link ModuleRemoteConfig.RemoteConfig} instance, never {@code null}: before init, or while the feature does not
* run, as its consent is not given or it is not enabled, one that ignores calls until it runs
*/
public ModuleRemoteConfig.RemoteConfig remoteConfig() {
if (!isInitialized()) {
if (L != null) {
L.e("[Countly] remoteConfig, SDK is not initialized yet.");
}
return null;
return SDKCore.dormantRemoteConfig;
}
return sdk.remoteConfig();
}

/**
* <code>DeviceId</code> interface to use device id functionalities.
*
* @return {@link ModuleDeviceIdCore.DeviceId} instance.
* @return {@link ModuleDeviceIdCore.DeviceId} instance, never {@code null}: before init, or while the feature does not
* run, as its consent is not given or it is not enabled, one that ignores calls until it runs
*/
public ModuleDeviceIdCore.DeviceId deviceId() {
if (!isInitialized()) {
if (L != null) {
L.e("[Countly] SDK is not initialized yet.");
}
return null;
return SDKCore.dormantDeviceId;
}
return sdk.deviceId();
}
Expand All @@ -472,44 +476,47 @@ public Event event(String key) {
/**
* Event module calls
*
* @return event module otherwise null if SDK is not initialized
* @return {@link ModuleEvents.Events} instance, never {@code null}: before init, or while the feature does not
* run, as its consent is not given or it is not enabled, one that ignores calls until it runs
*/
public ModuleEvents.Events events() {
if (!isInitialized()) {
if (L != null) {
L.e("[Countly] SDK is not initialized yet.");
}
return null;
return SDKCore.dormantEvents;
}
return sdk.events();
}

/**
* Crash module calls
*
* @return crash module otherwise null if SDK is not initialized
* @return {@link ModuleCrashes.Crashes} instance, never {@code null}: before init, or while the feature does not
* run, as its consent is not given or it is not enabled, one that ignores calls until it runs
*/
public ModuleCrashes.Crashes crashes() {
if (!isInitialized()) {
if (L != null) {
L.e("[Countly] SDK is not initialized yet.");
}
return null;
return SDKCore.dormantCrashes;
}
return sdk.crashes();
}

/**
* <code>Views</code> interface to use views feature.
*
* @return {@link ModuleViews.Views} instance.
* @return {@link ModuleViews.Views} instance, never {@code null}: before init, or while the feature does not
* run, as its consent is not given or it is not enabled, one that ignores calls until it runs
*/
public ModuleViews.Views views() {
if (!isInitialized()) {
if (L != null) {
L.e("[Countly] SDK is not initialized yet.");
}
return null;
return SDKCore.dormantViews;
}
return sdk.views();
}
Expand All @@ -530,30 +537,32 @@ public Event timedEvent(String key) {
/**
* <code>UserProfile</code> interface to use user profile feature.
*
* @return {@link ModuleUserProfile.UserProfile} instance.
* @return {@link ModuleUserProfile.UserProfile} instance, never {@code null}: before init, or while the feature does not
* run, as its consent is not given or it is not enabled, one that ignores calls until it runs
*/
public ModuleUserProfile.UserProfile userProfile() {
if (!isInitialized()) {
if (L != null) {
L.e("[Countly] userProfile, SDK is not initialized yet.");
}
return null;
return SDKCore.dormantUserProfile;
}
return sdk.userProfile();
}

/**
* <code>Location</code> interface to use location feature.
*
* @return {@link ModuleLocation.Location} instance.
* @return {@link ModuleLocation.Location} instance, never {@code null}: before init, or while the feature does not
* run, as its consent is not given or it is not enabled, one that ignores calls until it runs
*/
public ModuleLocation.Location location() {
if (!isInitialized()) {
// L is null before init, so this has to be guarded like every sibling accessor.
if (L != null) {
L.e("Countly.sharedInstance().init must be called before accessing location");
}
return null;
return SDKCore.dormantLocation;
}

return sdk.location();
Expand All @@ -574,25 +583,25 @@ public User user() {

@Override
public Usage addParam(String key, Object value) {
L.d("[Countly] addParam: key = " + key + " value = " + value);
L.d("[Countly] addParam: key = " + key);
return ((Session) sdk.session(null)).addParam(key, value);
}

@Override
public Usage addCrashReport(Throwable t, boolean fatal) {
L.d("[Countly] addCrashReport: t = " + t + " fatal = " + fatal);
L.d("[Countly] addCrashReport: fatal = " + fatal);
return ((Session) sdk.session(null)).addCrashReport(t, fatal);
}

@Override
public Usage addCrashReport(Throwable t, boolean fatal, String name, Map<String, String> segments, String... logs) {
L.d("[Countly] addCrashReport: t = " + t + " fatal = " + fatal + " name = " + name + " segments = " + segments + " logs = " + logs);
L.d("[Countly] addCrashReport: fatal = " + fatal);
return ((Session) sdk.session(null)).addCrashReport(t, fatal, name, segments, logs);
}

@Override
public Usage addLocation(double latitude, double longitude) {
L.d("[Countly] addLocation: latitude = " + latitude + " longitude = " + longitude);
L.d("[Countly] addLocation");
return ((Session) sdk.session(null)).addLocation(latitude, longitude);
}

Expand All @@ -603,7 +612,7 @@ public Usage addLocation(double latitude, double longitude) {
*/
@Override
public View view(String name, boolean start) {
L.d("[Countly] view: name = " + name + " start = " + start);
L.d("[Countly] view: start = " + start);
return ((Session) sdk.session(null)).view(name, start);
}

Expand All @@ -614,7 +623,7 @@ public View view(String name, boolean start) {
*/
@Override
public View view(String name) {
L.d("[Countly] view: name = " + name);
L.d("[Countly] view");
return ((Session) sdk.session(null)).view(name);
}
}
Loading
Loading