Skip to content
Draft
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 @@ -18,6 +18,10 @@
* Added support for objects as campaign data in `recordDirectAttribution` and as values in `recordIndirectAttribution`, in `Countly` and in `CountlyConfig`.
* Added the names "DEVELOPMENT", "PRODUCTION" and "ADHOC" as accepted values of `CountlyConfig.setPushTokenType`.
* Added `CountlyConfig.setUpdateSessionTimerDelay(delay)` to set how often, in seconds, the SDK sends automatic session updates and the queued events. The default is 60 seconds, and a value that is not a whole number from 1 to 2147483647 is ignored with an error log.
* Added `Countly.changeServerUrl(serverUrl)` to change the server URL after init.
* Added `Countly.changeAppKey(appKey)` to change the app key after init.
* Added `Countly.flushQueues()` to delete the stored requests and events.
* Added `Countly.attemptToSendStoredRequests()` to send the stored requests now.

* Mitigated remote config update issues:
* Updates without remote config consent, in temporary device ID mode or with SDK networking disabled now complete with an error in iOS, instead of never completing.
Expand Down Expand Up @@ -82,6 +86,7 @@
* `Countly.setUserData`, `Countly.userDataBulk.setUserProperties`, `Countly.userData.setProperty` and `Countly.userDataBulk.setProperty` now use the iOS SDK's user property API: pending events are sent before the user details, and `null` values and nested objects are skipped with a log line instead of being sent, as on Android.
* The iOS SDK's default log level is now debug instead of verbose.

* Updated the underlying Android SDK version to XX.XX.XX
* Updated the underlying iOS SDK version to 26.8.0

## 26.1.0
Expand Down
48 changes: 48 additions & 0 deletions Countly.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1069,6 +1069,54 @@ declare module "countly-sdk-react-native-bridge" {
*/
export function removeDifferentAppKeysFromQueue(): string | void;

/**
* Sends the requests to another server from now on, for example when the app moves to another server.
* The requests already in the queue, the immediate requests and the web views opened after the call use the new server too.
* The app key stays, and the URL is kept in memory only: the next init uses the one in the config.
* In Android, an invalid URL, or an "http" URL while certificate pinning is set, is ignored with an error log. After a change,
* the SDK sends the push token and the consent state to the new server and fetches the SDK behavior settings and the remote
* config values from it, as after init. In iOS, the URL is used as given, so pass a valid one, and call 'sendPushToken' and
* 'remoteConfig.update' again when the new server needs them.
* Should be called after init.
* @param {string} serverUrl the new server URL, for example "https://your.server.ly"
* @return {string | void} error message or void
*/
export function changeServerUrl(serverUrl: string): string | void;

/**
* Reports to another app from now on, for example when a white label app learns at login which customer it belongs to.
* Before the switch, the running views stop, the recorded events are packed into a request and the running session ends,
* all with the old app key. After it, a session begins for the new app if one was running, and the views start again
* unless 'disableViewRestartForManualRecording' is set without automatic view tracking.
* Requests already in the queue keep the app key they were created with: call 'replaceAllAppKeysInQueueWithCurrentAppKey'
* to send them to the new app, or 'removeDifferentAppKeysFromQueue' to drop them.
* The device ID, consent and server URL stay, and the key is kept in memory only: the next init uses the one in the config.
* In Android, the "YOUR_APP_KEY" placeholder and the current key are ignored, and the new app then gets the push token,
* the consent state, its SDK behavior settings and its remote config values, as after init. In iOS, only an automatic
* session ends and begins again, running APM traces are cancelled, and 'sendPushToken' and 'remoteConfig.update' have to
* be called again when the new app needs them.
* Should be called after init.
* @param {string} appKey the app key of the new app
* @return {string | void} error message or void
*/
export function changeAppKey(appKey: string): string | void;

/**
* Deletes the stored requests and the recorded events that are not in a request yet.
* Started timed events, running views and pending user profile changes are kept, and a request already on its way is not stopped.
* Should be called after init.
* @return {string | void} error message or void
*/
export function flushQueues(): string | void;

/**
* Packs the recorded events into a request and starts sending the stored requests now, without waiting for the next timer tick.
* The backoff mechanism and the temporary device ID mode still hold the requests back.
* Should be called after init.
* @return {string | void} error message or void
*/
export function attemptToSendStoredRequests(): string | void;

/**
* Call this function when app is loaded, so that the app launch duration can be recorded.
* Should be called after init.
Expand Down
90 changes: 90 additions & 0 deletions Countly.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@
const jsStackTrace = Utils.getStackTrace(error);
let errorTitle;
let stackArr;
if (jsStackTrace == null || jsStackTrace.length === 0) {

Check warning on line 156 in Countly.js

View workflow job for this annotation

GitHub Actions / checks (24)

Expected '===' and instead saw '=='

Check warning on line 156 in Countly.js

View workflow job for this annotation

GitHub Actions / checks (22)

Expected '===' and instead saw '=='
errorTitle = errorName;
stackArr = typeof error.stack === "string" ? error.stack : "";
} else {
Expand All @@ -169,7 +169,7 @@
}

const matches = row.match(regExp);
return matches && matches.length == 8 ? `${matches[1]}${matches[2]}${matches[4]}(${matches[6]}:${matches[7]})` : row;

Check warning on line 172 in Countly.js

View workflow job for this annotation

GitHub Actions / checks (24)

Expected '===' and instead saw '=='

Check warning on line 172 in Countly.js

View workflow job for this annotation

GitHub Actions / checks (22)

Expected '===' and instead saw '=='
});
stackArr = stackArr.join("\n");
}
Expand Down Expand Up @@ -777,7 +777,7 @@
return message;
}
keyName = keyName.toString();
if (keyName && (keyValue || keyValue == "")) {

Check warning on line 780 in Countly.js

View workflow job for this annotation

GitHub Actions / checks (24)

Expected '===' and instead saw '=='

Check warning on line 780 in Countly.js

View workflow job for this annotation

GitHub Actions / checks (22)

Expected '===' and instead saw '=='
await CountlyNativeModule.userData_setProperty([keyName, keyValue]);
}
};
Expand Down Expand Up @@ -938,7 +938,7 @@
return message;
}
keyValue = keyValue.toString();
if (keyValue || keyValue == "") {

Check warning on line 941 in Countly.js

View workflow job for this annotation

GitHub Actions / checks (24)

Expected '===' and instead saw '=='

Check warning on line 941 in Countly.js

View workflow job for this annotation

GitHub Actions / checks (22)

Expected '===' and instead saw '=='
await CountlyNativeModule.userData_setOnce([keyName, keyValue]);
}
};
Expand Down Expand Up @@ -967,7 +967,7 @@
return message;
}
keyValue = keyValue.toString();
if (keyValue || keyValue == "") {

Check warning on line 970 in Countly.js

View workflow job for this annotation

GitHub Actions / checks (24)

Expected '===' and instead saw '=='

Check warning on line 970 in Countly.js

View workflow job for this annotation

GitHub Actions / checks (22)

Expected '===' and instead saw '=='
await CountlyNativeModule.userData_pushUniqueValue([keyName, keyValue]);
}
};
Expand Down Expand Up @@ -996,7 +996,7 @@
return message;
}
keyValue = keyValue.toString();
if (keyValue || keyValue == "") {

Check warning on line 999 in Countly.js

View workflow job for this annotation

GitHub Actions / checks (24)

Expected '===' and instead saw '=='

Check warning on line 999 in Countly.js

View workflow job for this annotation

GitHub Actions / checks (22)

Expected '===' and instead saw '=='
await CountlyNativeModule.userData_pushValue([keyName, keyValue]);
}
};
Expand Down Expand Up @@ -1025,7 +1025,7 @@
return message;
}
keyValue = keyValue.toString();
if (keyValue || keyValue == "") {

Check warning on line 1028 in Countly.js

View workflow job for this annotation

GitHub Actions / checks (24)

Expected '===' and instead saw '=='

Check warning on line 1028 in Countly.js

View workflow job for this annotation

GitHub Actions / checks (22)

Expected '===' and instead saw '=='
await CountlyNativeModule.userData_pullValue([keyName, keyValue]);
}
};
Expand Down Expand Up @@ -1118,7 +1118,7 @@
return message;
}
keyName = keyName.toString();
if (keyName && (keyValue || keyValue == "")) {

Check warning on line 1121 in Countly.js

View workflow job for this annotation

GitHub Actions / checks (24)

Expected '===' and instead saw '=='

Check warning on line 1121 in Countly.js

View workflow job for this annotation

GitHub Actions / checks (22)

Expected '===' and instead saw '=='
await CountlyNativeModule.userDataBulk_setProperty([keyName, keyValue]);
}
};
Expand Down Expand Up @@ -1285,7 +1285,7 @@
return message;
}
keyValue = keyValue.toString();
if (keyValue || keyValue == "") {

Check warning on line 1288 in Countly.js

View workflow job for this annotation

GitHub Actions / checks (24)

Expected '===' and instead saw '=='

Check warning on line 1288 in Countly.js

View workflow job for this annotation

GitHub Actions / checks (22)

Expected '===' and instead saw '=='
await CountlyNativeModule.userDataBulk_setOnce([keyName, keyValue]);
}
};
Expand Down Expand Up @@ -1315,7 +1315,7 @@
return message;
}
keyValue = keyValue.toString();
if (keyValue || keyValue == "") {

Check warning on line 1318 in Countly.js

View workflow job for this annotation

GitHub Actions / checks (24)

Expected '===' and instead saw '=='

Check warning on line 1318 in Countly.js

View workflow job for this annotation

GitHub Actions / checks (22)

Expected '===' and instead saw '=='
await CountlyNativeModule.userDataBulk_pushUniqueValue([keyName, keyValue]);
}
};
Expand Down Expand Up @@ -1726,6 +1726,96 @@
CountlyNativeModule.removeDifferentAppKeysFromQueue();
};

/**
* Sends the requests to another server from now on, for example when the app moves to another server.
* The requests already in the queue, the immediate requests and the web views opened after the call use the new server too.
* The app key stays, and the URL is kept in memory only: the next init uses the one in the config.
* In Android, an invalid URL, or an "http" URL while certificate pinning is set, is ignored with an error log. After a change,
* the SDK sends the push token and the consent state to the new server and fetches the SDK behavior settings and the remote
* config values from it, as after init. In iOS, the URL is used as given, so pass a valid one, and call 'sendPushToken' and
* 'remoteConfig.update' again when the new server needs them.
* Should be called after init.
* @param {string} serverUrl the new server URL, for example "https://your.server.ly"
* @return {string | void} error message or void
*/
Countly.changeServerUrl = function (serverUrl) {
if (!_state.isInitialized) {
const message = "'init' must be called before 'changeServerUrl'";
L.e(`changeServerUrl, ${message}`);
return message;
}
const message = Validate.String(serverUrl, "serverUrl", "changeServerUrl");
if (message) {
L.w(`changeServerUrl, ${message}`);
return message;
}
L.i(`changeServerUrl, Changing the server URL to: [${serverUrl}]`);
CountlyNativeModule.changeServerUrl([serverUrl]);
};

/**
* Reports to another app from now on, for example when a white label app learns at login which customer it belongs to.
* Before the switch, the running views stop, the recorded events are packed into a request and the running session ends,
* all with the old app key. After it, a session begins for the new app if one was running, and the views start again
* unless 'disableViewRestartForManualRecording' is set without automatic view tracking.
* Requests already in the queue keep the app key they were created with: call 'replaceAllAppKeysInQueueWithCurrentAppKey'
* to send them to the new app, or 'removeDifferentAppKeysFromQueue' to drop them.
* The device ID, consent and server URL stay, and the key is kept in memory only: the next init uses the one in the config.
* In Android, the "YOUR_APP_KEY" placeholder and the current key are ignored, and the new app then gets the push token,
* the consent state, its SDK behavior settings and its remote config values, as after init. In iOS, only an automatic
* session ends and begins again, running APM traces are cancelled, and 'sendPushToken' and 'remoteConfig.update' have to
* be called again when the new app needs them.
* Should be called after init.
* @param {string} appKey the app key of the new app
* @return {string | void} error message or void
*/
Countly.changeAppKey = function (appKey) {
if (!_state.isInitialized) {
const message = "'init' must be called before 'changeAppKey'";
L.e(`changeAppKey, ${message}`);
return message;
}
const message = Validate.String(appKey, "appKey", "changeAppKey");
if (message) {
L.w(`changeAppKey, ${message}`);
return message;
}
L.i(`changeAppKey, Changing the app key to: [${appKey}]`);
CountlyNativeModule.changeAppKey([appKey]);
};

/**
* Deletes the stored requests and the recorded events that are not in a request yet.
* Started timed events, running views and pending user profile changes are kept, and a request already on its way is not stopped.
* Should be called after init.
* @return {string | void} error message or void
*/
Countly.flushQueues = function () {
if (!_state.isInitialized) {
const message = "'init' must be called before 'flushQueues'";
L.e(`flushQueues, ${message}`);
return message;
}
L.i("flushQueues, Deleting the stored requests and the recorded events");
CountlyNativeModule.flushQueues();
};

/**
* Packs the recorded events into a request and starts sending the stored requests now, without waiting for the next timer tick.
* The backoff mechanism and the temporary device ID mode still hold the requests back.
* Should be called after init.
* @return {string | void} error message or void
*/
Countly.attemptToSendStoredRequests = function () {
if (!_state.isInitialized) {
const message = "'init' must be called before 'attemptToSendStoredRequests'";
L.e(`attemptToSendStoredRequests, ${message}`);
return message;
}
L.i("attemptToSendStoredRequests, Sending the recorded events and the stored requests");
CountlyNativeModule.attemptToSendStoredRequests();
};

/**
* Call this function when app is loaded, so that the app launch duration can be recorded.
* Should be called after init.
Expand Down
48 changes: 48 additions & 0 deletions __tests__/countly.bridge.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ function createNativeModuleMock(overrides = {}) {
recordDirectAttribution: jest.fn(),
recordIndirectAttribution: jest.fn(),
removeDifferentAppKeysFromQueue: jest.fn(),
changeServerUrl: jest.fn(),
changeAppKey: jest.fn(),
flushQueues: jest.fn(),
attemptToSendStoredRequests: jest.fn(),
appLoadingFinished: jest.fn(),
setCustomMetrics: jest.fn(),
enterContentZone: jest.fn(),
Expand Down Expand Up @@ -548,6 +552,50 @@ test('notification, location, trace, and attribution helpers forward expected pa
expect(typeof notificationSubscription.remove).toBe('function');
});

test('server URL, app key and queue calls guard before init, refuse a missing value and leave the rest to the native SDK', async () => {
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
const infoSpy = jest.spyOn(console, 'info').mockImplementation(() => {});
const debugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {});
const { Countly, CountlyConfig, nativeModule } = loadCountlyBridge({ platform: 'android' });
const config = new CountlyConfig('https://server.count.ly', 'app-key').setLoggingEnabled(true);

expect(Countly.changeServerUrl('https://other.count.ly')).toBe("'init' must be called before 'changeServerUrl'");
expect(Countly.changeAppKey('other-app-key')).toBe("'init' must be called before 'changeAppKey'");
expect(Countly.flushQueues()).toBe("'init' must be called before 'flushQueues'");
expect(Countly.attemptToSendStoredRequests()).toBe("'init' must be called before 'attemptToSendStoredRequests'");
expect(errorSpy).toHaveBeenCalledWith("[CountlyReactNative] changeServerUrl, 'init' must be called before 'changeServerUrl'");

await Countly.initWithConfig(config);

expect(Countly.changeServerUrl('')).toBe('serverUrl should not be null, undefined or empty');
expect(Countly.changeServerUrl(undefined)).toBe('serverUrl should not be null, undefined or empty');
expect(Countly.changeServerUrl(42)).toBe("skipping value for 'serverUrl', due to unsupported data type 'number', its data type should be 'string'");
expect(Countly.changeAppKey(null)).toBe('appKey should not be null, undefined or empty');
expect(Countly.changeAppKey({ key: 'app-key-2' })).toBe("skipping value for 'appKey', due to unsupported data type 'object', its data type should be 'string'");
expect(warnSpy).toHaveBeenCalledWith('[CountlyReactNative] changeServerUrl, serverUrl should not be null, undefined or empty');
expect(warnSpy).toHaveBeenCalledWith('[CountlyReactNative] changeAppKey, appKey should not be null, undefined or empty');
expect(nativeModule.changeServerUrl).not.toHaveBeenCalled();
expect(nativeModule.changeAppKey).not.toHaveBeenCalled();
expect(nativeModule.flushQueues).not.toHaveBeenCalled();
expect(nativeModule.attemptToSendStoredRequests).not.toHaveBeenCalled();

expect(Countly.changeServerUrl('https://other.count.ly///')).toBeUndefined();
expect(Countly.changeServerUrl('ftp://other.count.ly')).toBeUndefined();
expect(Countly.changeAppKey('YOUR_APP_KEY')).toBeUndefined();
expect(Countly.changeAppKey('app-key-2')).toBeUndefined();
expect(Countly.flushQueues()).toBeUndefined();
expect(Countly.attemptToSendStoredRequests()).toBeUndefined();

expect(nativeModule.changeServerUrl.mock.calls).toEqual([[['https://other.count.ly///']], [['ftp://other.count.ly']]]);
expect(nativeModule.changeAppKey.mock.calls).toEqual([[['YOUR_APP_KEY']], [['app-key-2']]]);
expect(nativeModule.flushQueues).toHaveBeenCalledWith();
expect(nativeModule.attemptToSendStoredRequests).toHaveBeenCalledWith();
expect(infoSpy).toHaveBeenCalledWith('[CountlyReactNative] changeAppKey, Changing the app key to: [app-key-2]');

[warnSpy, errorSpy, infoSpy, debugSpy].forEach((spy) => spy.mockRestore());
});

test('metrics, crash, and custom metric helpers normalize payloads', async () => {
const { Countly, CountlyConfig, nativeModule } = loadCountlyBridge();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2366,6 +2366,40 @@ public void removeDifferentAppKeysFromQueue() {
Countly.sharedInstance().requestQueue().eraseWrongAppKeyRequests();
}

/**
* Sends the requests to another server from now on. The Android SDK validates the URL.
*
* @param args the new server URL at index 0
*/
public void changeServerUrl(ReadableArray args) {
String serverUrl = args.getString(0);
Countly.sharedInstance().requestQueue().changeServerUrl(serverUrl);
}

/**
* Reports to another app from now on. The Android SDK validates the key and closes what belongs to the old app first.
*
* @param args the app key of the new app at index 0
*/
public void changeAppKey(ReadableArray args) {
String appKey = args.getString(0);
Countly.sharedInstance().requestQueue().changeAppKey(appKey);
}

/**
* Deletes the stored requests and the recorded events that are not in a request yet.
*/
public void flushQueues() {
Countly.sharedInstance().requestQueue().flushQueues();
}

/**
* Packs the recorded events into a request and starts sending the stored requests now.
*/
public void attemptToSendStoredRequests() {
Countly.sharedInstance().requestQueue().attemptToSendStoredRequests();
}


public void setEventSendThreshold(ReadableArray args) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,42 @@ public void removeDifferentAppKeysFromQueue() {
this.impl.removeDifferentAppKeysFromQueue();
}

/**
* Sends the requests to another server from now on.
*
* @param args the new server URL at index 0
*/
@Override
public void changeServerUrl(ReadableArray args) {
this.impl.changeServerUrl(args);
}

/**
* Reports to another app from now on.
*
* @param args the app key of the new app at index 0
*/
@Override
public void changeAppKey(ReadableArray args) {
this.impl.changeAppKey(args);
}

/**
* Deletes the stored requests and the recorded events that are not in a request yet.
*/
@Override
public void flushQueues() {
this.impl.flushQueues();
}

/**
* Packs the recorded events into a request and starts sending the stored requests now.
*/
@Override
public void attemptToSendStoredRequests() {
this.impl.attemptToSendStoredRequests();
}

@Override
public void setEventSendThreshold(ReadableArray args) {
this.impl.setEventSendThreshold(args);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,42 @@ public void removeDifferentAppKeysFromQueue() {
this.impl.removeDifferentAppKeysFromQueue();
}

/**
* Sends the requests to another server from now on.
*
* @param args the new server URL at index 0
*/
@ReactMethod
public void changeServerUrl(ReadableArray args) {
this.impl.changeServerUrl(args);
}

/**
* Reports to another app from now on.
*
* @param args the app key of the new app at index 0
*/
@ReactMethod
public void changeAppKey(ReadableArray args) {
this.impl.changeAppKey(args);
}

/**
* Deletes the stored requests and the recorded events that are not in a request yet.
*/
@ReactMethod
public void flushQueues() {
this.impl.flushQueues();
}

/**
* Packs the recorded events into a request and starts sending the stored requests now.
*/
@ReactMethod
public void attemptToSendStoredRequests() {
this.impl.attemptToSendStoredRequests();
}

@ReactMethod
public void setEventSendThreshold(ReadableArray args) {
this.impl.setEventSendThreshold(args);
Expand Down
Loading
Loading