Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
```
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
Expand All @@ -41,6 +42,10 @@
* 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.

## 26.8.0
Expand Down
57 changes: 33 additions & 24 deletions sdk-java/src/main/java/ly/count/sdk/java/Countly.java
Original file line number Diff line number Diff line change
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public interface ConfigurationProvider {
/**
* Key {@code cet}.
*
* @return whether custom events, the ones whose key does not start with {@code [CLY]_}, may be recorded
* @return whether custom events, the ones whose key is not one of {@link ModuleEvents#INTERNAL_EVENT_KEYS}, may be recorded
*/
boolean getCustomEventTrackingEnabled();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,14 @@ protected void startTimer(long timerDelay, long initialDelayMs, Runnable runnabl
startTime = 0;
}

timerService.scheduleWithFixedDelay(runnable, startTime, delay, TimeUnit.MILLISECONDS);
timerService.scheduleWithFixedDelay(() -> {
try {
runnable.run();
} catch (Throwable t) {
//a fixed delay schedule ends for good once a run throws
L.e("[CountlyTimer] a timer run failed, [" + t + "]");
}
}, startTime, delay, TimeUnit.MILLISECONDS);
}

/**
Expand Down
26 changes: 26 additions & 0 deletions sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleBase.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package ly.count.sdk.java.internal;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import ly.count.sdk.java.Session;

/**
Expand All @@ -13,6 +15,9 @@
* </ul>
*/
public abstract class ModuleBase {
//what a call through an interface while its module does not run hands to its callback
static final String NOT_RUNNING_ERROR = "The feature does not run, as its consent is not given or it is not enabled";

private boolean active = false;

protected Log L = null;
Expand Down Expand Up @@ -47,6 +52,27 @@ public void init(InternalConfig config) {
public void stop(InternalConfig config, boolean clear) {
}

/**
* The instance of this module that the SDK runs, which every call through an interface of this
* module goes to: this one, or the running one when the interface was handed out before that
* started or kept from an instance that stopped. A call while none runs, as before init or while
* the consent of the feature is not given, is ignored and logged.
*
* @param call the call, starting with the bracketed name of the interface, for the log line
* @param <M> the module type
* @return the running instance, {@code null} while none runs
*/
@SuppressWarnings("unchecked")
@Nullable
<M extends ModuleBase> M runningInstance(@Nonnull String call) {
SDKCore core = SDKCore.instance;
ModuleBase running = core == null ? null : core.module(getClass());
if (running == null && core != null && core.L != null) {
core.L.w(call + ", the feature does not run, as its consent is not given or it is not enabled, ignoring the call");
}
return (M) running;
}

/**
* A method to be used by the module itself to determine if {@link #init(InternalConfig)} initialized it
* and haven't been stopped yet by {@link #stop(InternalConfig, boolean)}.
Expand Down
Loading