diff --git a/.gitignore b/.gitignore
index 23f329c..f1acec4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
node_modules/
dist/
rollup.config.js
+Countly.d.ts
```
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ffc053f..d348080 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,36 @@
+## 24.11.0
+
+* Mitigated an issue where SDK could try to send old stored offline mode data during init if `clear_stored_id` was true
+* Mitigated an issue where the SDK could stayed on offline mode after the first init with `offline_mode` set to true
+* Mitigated an issue where old Rating widget stickers were not cleared when a new one was presented
+
+* Improved view tracking logic
+* Default request method is now set to "POST"
+* Healtchecks won't be sent in offline mode anymore
+* Added a new interface 'feedback' which includes convenience methods to show feedback widgets:
+ * showNPS([String nameIDorTag]) - for displaying the first available NPS widget or one with the given name, Tag or ID value
+ * showSurvey([String nameIDorTag]) - for displaying the first available Survey widget or one with the given name, Tag or ID value
+ * showRating([String nameIDorTag]) - for displaying the first available Rating widget or one with the given name, Tag or ID value
+
+## 24.4.1
+
+* Added a new method `set_id(newDeviceId)` for managing device id changes according to the device ID Type.
+
+## 24.4.0
+
+! Minor breaking change ! For implementations using `salt` the browser compatibility is tied to SubtleCrypto's `digest` method support
+
+* Added the `salt` init config flag to add checksums to requests (for secure contexts only)
+* Added support for Feedback Widgets' terms and conditions
+
+## 23.12.6
+
+* Mitigated an issue where error tracking could prevent SDK initialization in async mode
+
+## 23.12.5
+
+* Mitigated an issue where the SDK was not emptying the async queue explicity when closing a browser
+
## 23.12.4
* Enhanced userAgentData detection for bot filtering
diff --git a/cypress/e2e/async_queue.cy.js b/cypress/e2e/async_queue.cy.js
new file mode 100644
index 0000000..818ec59
--- /dev/null
+++ b/cypress/e2e/async_queue.cy.js
@@ -0,0 +1,289 @@
+/* eslint-disable require-jsdoc */
+var Countly = require("../../Countly.js");
+var hp = require("../support/helper.js");
+
+function initMain(clear) {
+ Countly.init({
+ app_key: "YOUR_APP_KEY",
+ url: "https://your.domain.count.ly",
+ debug: true,
+ test_mode: true,
+ clear_stored_id: clear
+ });
+}
+
+function event(number) {
+ return {
+ key: `event_${number}`,
+ segmentation: {
+ id: number
+ }
+ };
+};
+
+
+// All the tests below checks if the functions are working correctly
+// Currently tests for 'beforeunload' and 'unload' events has to be done manually by using the throttling option of the browser
+describe("Test Countly.q related methods and processes", () => {
+ // For this tests we disable the internal heatbeat and use processAsyncQueue and sendEventsForced
+ // So we are able to test if those functions work as intented:
+ // processAsyncQueue should send events from .q to event queue
+ // sendEventsForced should send events from event queue to request queue (it also calls processAsyncQueue)
+ it("Check processAsyncQueue and sendEventsForced works as expected", () => {
+ hp.haltAndClearStorage(() => {
+ // Disable heartbeat and init the SDK
+ Countly.noHeartBeat = true;
+ initMain();
+ cy.wait(1000);
+
+ // Check that the .q is empty
+ expect(Countly.q.length).to.equal(0);
+
+ // Add 4 events to the .q
+ Countly.q.push(['track_errors']); // adding this as calling it during init used to cause an error (at v23.12.5)
+ Countly.q.push(['add_event', event(1)]);
+ Countly.q.push(['add_event', event(2)]);
+ Countly.q.push(['add_event', event(3)]);
+ Countly.q.push(['add_event', event(4)]);
+ // Check that the .q has 4 events
+ expect(Countly.q.length).to.equal(5);
+
+ cy.fetch_local_event_queue().then((rq) => {
+ // Check that events are still in .q
+ expect(Countly.q.length).to.equal(5);
+
+ // Check that the event queue is empty
+ expect(rq.length).to.equal(0);
+
+ // Process the .q (should send things to the event queue)
+ Countly._internals.processAsyncQueue();
+
+ // Check that the .q is empty
+ expect(Countly.q.length).to.equal(0);
+
+ cy.fetch_local_request_queue().then((rq) => {
+ // Check that nothing sent to request queue
+ expect(rq.length).to.equal(0);
+ cy.fetch_local_event_queue().then((eq) => {
+ // Check that events are now in event queue
+ expect(eq.length).to.equal(4);
+
+ // Send events from event queue to request queue
+ Countly._internals.sendEventsForced();
+ cy.fetch_local_event_queue().then((eq) => {
+ // Check that event queue is empty
+ expect(eq.length).to.equal(0);
+ cy.fetch_local_request_queue().then((rq) => {
+ // Check that events are now in request queue
+ expect(rq.length).to.equal(1);
+ const eventsArray = JSON.parse(rq[0].events);
+ expect(eventsArray[0].key).to.equal("event_1");
+ expect(eventsArray[1].key).to.equal("event_2");
+ expect(eventsArray[2].key).to.equal("event_3");
+ expect(eventsArray[3].key).to.equal("event_4");
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ //This test is same with the ones above but this time we use change_id to trigger processAsyncQueue
+ it('Check changing device ID without merge empties the .q', () => {
+ hp.haltAndClearStorage(() => {
+ // Disable heartbeat and init the SDK
+ Countly.noHeartBeat = true;
+ Countly.q = [];
+ initMain();
+ cy.wait(1000);
+
+ // Check that the .q is empty
+ expect(Countly.q.length).to.equal(0);
+
+ // Add 4 events to the .q
+ Countly.q.push(['add_event', event(1)]);
+ Countly.q.push(['add_event', event(2)]);
+ Countly.q.push(['add_event', event(3)]);
+ Countly.q.push(['add_event', event(4)]);
+ // Check that the .q has 4 events
+ expect(Countly.q.length).to.equal(4);
+
+ cy.fetch_local_event_queue().then((rq) => {
+ // Check that the event queue is empty
+ expect(rq.length).to.equal(0);
+
+ // Check that events are still in .q
+ expect(Countly.q.length).to.equal(4);
+
+ // Trigger processAsyncQueue by changing device ID without merge
+ Countly.change_id("new_user_id", false);
+
+ // Check that the .q is empty
+ expect(Countly.q.length).to.equal(0);
+ cy.fetch_local_event_queue().then((eq) => {
+ // Check that event queue has new device ID's orientation event
+ expect(eq.length).to.equal(1);
+ expect(eq[0].key).to.equal("[CLY]_orientation");
+ cy.fetch_local_request_queue().then((rq) => {
+ // Check that events are now in request queue (second request is begin session for new device ID)
+ expect(rq.length).to.equal(2);
+ const eventsArray = JSON.parse(rq[0].events);
+ expect(eventsArray[0].key).to.equal("event_1");
+ expect(eventsArray[1].key).to.equal("event_2");
+ expect(eventsArray[2].key).to.equal("event_3");
+ expect(eventsArray[3].key).to.equal("event_4");
+ // check begin session
+ expect(rq[1].begin_session).to.equal(1);
+ });
+ });
+ });
+ });
+ });
+ // This test checks if calling user_details triggers processAsyncQueue (it sends events from .q to event queue and then to request queue)
+ it('Check sending user details empties .q', () => {
+ hp.haltAndClearStorage(() => {
+ // Disable heartbeat and init the SDK
+ Countly.noHeartBeat = true;
+ Countly.q = [];
+ initMain();
+ cy.wait(1000);
+
+ // Check that the .q is empty
+ expect(Countly.q.length).to.equal(0);
+
+ // Add 4 events to the .q
+ Countly.q.push(['add_event', event(1)]);
+ Countly.q.push(['add_event', event(2)]);
+ Countly.q.push(['add_event', event(3)]);
+ Countly.q.push(['add_event', event(4)]);
+ // Check that the .q has 4 events
+ expect(Countly.q.length).to.equal(4);
+
+ cy.fetch_local_event_queue().then((rq) => {
+ // Check that the event queue is empty
+ expect(rq.length).to.equal(0);
+
+ // Check that events are still in .q
+ expect(Countly.q.length).to.equal(4);
+
+ // Trigger processAsyncQueue by adding user details
+ Countly.user_details({name: "test_user"});
+
+ // Check that the .q is empty
+ expect(Countly.q.length).to.equal(0);
+ cy.fetch_local_event_queue().then((eq) => {
+ // Check that event queue is empty
+ expect(eq.length).to.equal(0);
+ cy.fetch_local_request_queue().then((rq) => {
+ // Check that events are now in request queue (second request is user details)
+ expect(rq.length).to.equal(2);
+ const eventsArray = JSON.parse(rq[0].events);
+ expect(eventsArray[0].key).to.equal("event_1");
+ expect(eventsArray[1].key).to.equal("event_2");
+ expect(eventsArray[2].key).to.equal("event_3");
+ expect(eventsArray[3].key).to.equal("event_4");
+ // check user details
+ const user_details = JSON.parse(rq[1].user_details);
+ expect(user_details.name).to.equal("test_user");
+ });
+ });
+ });
+ });
+ });
+ // This Test checks if calling userData.save triggers processAsyncQueue (it sends events from .q to event queue and then to request queue)
+ it('Check sending custom user info empties .q', () => {
+ hp.haltAndClearStorage(() => {
+ // Disable heartbeat and init the SDK
+ Countly.noHeartBeat = true;
+ Countly.q = [];
+ initMain();
+ cy.wait(1000);
+
+ // Check that the .q is empty
+ expect(Countly.q.length).to.equal(0);
+
+ // Add 4 events to the .q
+ Countly.q.push(['add_event', event(1)]);
+ Countly.q.push(['add_event', event(2)]);
+ Countly.q.push(['add_event', event(3)]);
+ Countly.q.push(['add_event', event(4)]);
+ // Check that the .q has 4 events
+ expect(Countly.q.length).to.equal(4);
+
+ cy.fetch_local_event_queue().then((rq) => {
+ // Check that the event queue is empty
+ expect(rq.length).to.equal(0);
+
+ // Check that events are still in .q
+ expect(Countly.q.length).to.equal(4);
+
+ // Trigger processAsyncQueue by saving UserData
+ Countly.userData.set("name", "test_user");
+ Countly.userData.save();
+
+ // Check that the .q is empty
+ expect(Countly.q.length).to.equal(0);
+ cy.fetch_local_event_queue().then((eq) => {
+ // Check that event queue is empty
+ expect(eq.length).to.equal(0);
+ cy.fetch_local_request_queue().then((rq) => {
+ // Check that events are now in request queue (second request is user details)
+ expect(rq.length).to.equal(2);
+ const eventsArray = JSON.parse(rq[0].events);
+ expect(eventsArray[0].key).to.equal("event_1");
+ expect(eventsArray[1].key).to.equal("event_2");
+ expect(eventsArray[2].key).to.equal("event_3");
+ expect(eventsArray[3].key).to.equal("event_4");
+ // check user data
+ const user_details = JSON.parse(rq[1].user_details);
+ expect(user_details.custom.name).to.equal("test_user");
+ });
+ });
+ });
+ });
+ });
+ // This test check if the heartbeat is processing the .q (executes processAsyncQueue)
+ it('Check if heatbeat is processing .q', () => {
+ hp.haltAndClearStorage(() => {
+ // init the SDK
+ Countly.q = [];
+ initMain();
+
+ // Check that the .q is empty
+ expect(Countly.q.length).to.equal(0);
+ cy.fetch_local_event_queue().then((eq) => {
+ // Check that the event queue is empty
+ expect(eq.length).to.equal(0);
+ cy.fetch_local_request_queue().then((rq) => {
+ // Check that the request queue is empty
+ expect(rq.length).to.equal(0);
+ // Add 4 events to the .q
+ Countly.q.push(['add_event', event(1)]);
+ Countly.q.push(['add_event', event(2)]);
+ Countly.q.push(['add_event', event(3)]);
+ Countly.q.push(['add_event', event(4)]);
+ // Check that the .q has 4 events
+ expect(Countly.q.length).to.equal(4);
+ // Wait for heartBeat to process the .q
+ cy.wait(1500).then(() => {
+ // Check that the .q is empty
+ expect(Countly.q.length).to.equal(0);
+ cy.fetch_local_event_queue().then((eq) => {
+ // Check that event queue is empty as all must be in request queue
+ expect(eq.length).to.equal(0);
+ cy.fetch_local_request_queue().then((rq) => {
+ // Check that events are now in request queue
+ expect(rq.length).to.equal(1);
+ const eventsArray = JSON.parse(rq[0].events);
+ expect(eventsArray[0].key).to.equal("event_1");
+ expect(eventsArray[1].key).to.equal("event_2");
+ expect(eventsArray[2].key).to.equal("event_3");
+ expect(eventsArray[3].key).to.equal("event_4");
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+});
\ No newline at end of file
diff --git a/cypress/e2e/device_id_change.cy.js b/cypress/e2e/device_id_change.cy.js
index 44dcc3a..e99e7c2 100644
--- a/cypress/e2e/device_id_change.cy.js
+++ b/cypress/e2e/device_id_change.cy.js
@@ -145,6 +145,14 @@ describe("Device ID change tests ", ()=>{
});
});
});
+ it("Check init time temp mode with set_id", () => {
+ hp.haltAndClearStorage(() => {
+ initMain(true); // init in offline mode
+ testDeviceIdInReqs(() => {
+ Countly.set_id("new ID");
+ });
+ });
+ });
// ========================================
// default init configuration tests
@@ -209,4 +217,61 @@ describe("Device ID change tests ", ()=>{
});
});
});
-});
\ No newline at end of file
+});
+
+describe("Set ID change tests ", () => {
+ it('set_id should be non merge as there was dev provided id', () => {
+ hp.haltAndClearStorage(() => {
+ Countly.init({
+ app_key: "YOUR_APP_KEY",
+ url: "https://your.domain.count.ly",
+ test_mode: true,
+ debug: true,
+ device_id: "old ID"
+ });
+ Countly.add_event(eventObj("1")); // record an event.
+ cy.wait(500); // wait for the request to be sent
+ cy.fetch_local_request_queue().then((eq) => {
+ expect(eq[0].device_id).to.equal("old ID");
+ Countly.set_id("new ID");
+ Countly.add_event(eventObj("2")); // record another event
+ cy.wait(500); // wait for the request to be sent
+ cy.fetch_local_request_queue().then((eq2) => {
+ expect(eq2.length).to.equal(3); // no merge request
+ expect(eq2[0].device_id).to.equal("old ID");
+ expect(eq2[0].events).to.contains('"key\":\"1\"');
+ expect(eq2[1].device_id).to.equal("new ID");
+ expect(eq2[1].begin_session).to.equal(1);
+ expect(eq2[2].device_id).to.equal("new ID");
+ expect(eq2[2].events).to.contains('"key\":\"2\"');
+ });
+ });
+ });
+ });
+ it('set_id should be merge as there was sdk generated id', () => {
+ hp.haltAndClearStorage(() => {
+ initMain(false); // init normally
+ Countly.add_event(eventObj("1")); // record an event.
+ cy.wait(500); // wait for the request to be sent
+ let generatedID;
+ cy.fetch_local_request_queue().then((eq) => {
+ cy.log(eq);
+ generatedID = eq[0].device_id; // get the new id from first item in the queue
+ Countly.set_id("new ID");
+ Countly.add_event(eventObj("2")); // record another event
+ cy.wait(500); // wait for the request to be sent
+ cy.fetch_local_request_queue().then((eq2) => {
+ cy.log(eq2);
+ expect(eq2.length).to.equal(3); // merge request
+ expect(eq2[0].device_id).to.equal(generatedID);
+ expect(eq2[0].events).to.contains('"key\":\"1\"');
+ expect(eq2[1].device_id).to.equal("new ID");
+ expect(eq2[1].old_device_id).to.equal(generatedID);
+ expect(eq2[2].device_id).to.equal("new ID");
+ expect(eq2[2].events).to.contains('"key\":\"2\"');
+ });
+ });
+ });
+ });
+
+});
diff --git a/cypress/e2e/device_id_init_scenarios.cy.js b/cypress/e2e/device_id_init_scenarios.cy.js
index feb4776..6459293 100644
--- a/cypress/e2e/device_id_init_scenarios.cy.js
+++ b/cypress/e2e/device_id_init_scenarios.cy.js
@@ -8,21 +8,21 @@
* | device ID | generated | mode was | device ID | device ID | | not | |
* | was set | ID | enabled | provided | enabled | | set | set |
* +--------------------------------------------------+------------------------------------+----------------------+
- * | First init | - | - | - | 1 | - |
+ * | First init | - | - | - | 1 | 65 |
* +--------------------------------------------------+------------------------------------+----------------------+
- * | First init | x | - | - | 2 | - |
+ * | First init | x | - | - | 2 | 66 |
* +--------------------------------------------------+------------------------------------+----------------------+
- * | First init | - | x | - | 3 | - |
+ * | First init | - | x | - | 3 | 67 |
* +--------------------------------------------------+------------------------------------+----------------------+
- * | First init | - | - | x | 4 | - |
+ * | First init | - | - | x | 4 | 68 |
* +--------------------------------------------------+------------------------------------+----------------------+
- * | First init | x | x | - | 5 | - |
+ * | First init | x | x | - | 5 | 69 |
* +--------------------------------------------------+------------------------------------+----------------------+
- * | First init | x | - | x | 6 | - |
+ * | First init | x | - | x | 6 | 70 |
* +--------------------------------------------------+------------------------------------+----------------------+
- * | First init | - | x | x | 7 | - |
+ * | First init | - | x | x | 7 | 71 |
* +--------------------------------------------------+------------------------------------+----------------------+
- * | First init | x | x | x | 8 | - |
+ * | First init | x | x | x | 8 | 72 |
* +--------------------------------------------------+------------------------------------+----------------------+
* | x | - | - | - | - | - | 17 | 33 |
* +--------------------------------------------------+------------------------------------+----------------------+
@@ -720,12 +720,12 @@ describe("Device Id tests during first init", ()=>{
initMain("[CLY]_temp_id", false, undefined);
Countly.halt();
initMain(undefined, false, undefined, true);
- expect(Countly.get_device_id_type()).to.equal(Countly.DeviceIdType.SDK_GENERATED);
- validateSdkGeneratedId(Countly.get_device_id());
- validateInternalDeviceIdType(DeviceIdTypeInternalEnumsTest.SDK_GENERATED);
+ expect(Countly.get_device_id_type()).to.equal(Countly.DeviceIdType.TEMPORARY_ID);
+ expect(Countly.get_device_id()).to.eq("[CLY]_temp_id");
+ validateInternalDeviceIdType(DeviceIdTypeInternalEnumsTest.TEMPORARY_ID);
Countly.begin_session();
cy.fetch_local_request_queue().then((eq) => {
- checkRequestsForT(eq, DeviceIdTypeInternalEnumsTest.SDK_GENERATED);
+ checkRequestsForT(eq, DeviceIdTypeInternalEnumsTest.TEMPORARY_ID);
});
});
});
@@ -1080,4 +1080,104 @@ describe("Device Id tests during first init", ()=>{
});
});
});
+
+ // testing first 8 tests with clear_stored_id flag set to true
+ it("65-SDK is initialized without custom device id, without offline mode, without utm device id", () => {
+ hp.haltAndClearStorage(() => {
+ initMain(undefined, false, undefined, true);
+ expect(Countly.get_device_id_type()).to.eq(Countly.DeviceIdType.SDK_GENERATED);
+ validateSdkGeneratedId(Countly.get_device_id());
+ validateInternalDeviceIdType(DeviceIdTypeInternalEnumsTest.SDK_GENERATED);
+ Countly.begin_session();
+ cy.fetch_local_request_queue().then((eq) => {
+ checkRequestsForT(eq, DeviceIdTypeInternalEnumsTest.SDK_GENERATED);
+ });
+ });
+ });
+ // we provide device id information sdk should use it
+ it("66-SDK is initialized with custom device id, without offline mode, without utm device id", () => {
+ hp.haltAndClearStorage(() => {
+ initMain("gerwutztreimer", false, undefined, true);
+ expect(Countly.get_device_id_type()).to.equal(Countly.DeviceIdType.DEVELOPER_SUPPLIED);
+ expect(Countly.get_device_id()).to.eq("gerwutztreimer");
+ validateInternalDeviceIdType(DeviceIdTypeInternalEnumsTest.DEVELOPER_SUPPLIED);
+ Countly.begin_session();
+ cy.fetch_local_request_queue().then((eq) => {
+ checkRequestsForT(eq, DeviceIdTypeInternalEnumsTest.DEVELOPER_SUPPLIED);
+ });
+ });
+ });
+ // we provide no device id information sdk should generate the id
+ it("67-SDK is initialized without custom device id, with offline mode, without utm device id", () => {
+ hp.haltAndClearStorage(() => {
+ initMain(undefined, true, undefined, true);
+ expect(Countly.get_device_id_type()).to.equal(Countly.DeviceIdType.TEMPORARY_ID);
+ expect(Countly.get_device_id()).to.eq("[CLY]_temp_id");
+ validateInternalDeviceIdType(DeviceIdTypeInternalEnumsTest.TEMPORARY_ID);
+ Countly.begin_session();
+ cy.fetch_local_request_queue().then((eq) => {
+ checkRequestsForT(eq, DeviceIdTypeInternalEnumsTest.TEMPORARY_ID);
+ });
+ });
+ });
+ it("68-SDK is initialized without custom device id, without offline mode, with utm device id", () => {
+ hp.haltAndClearStorage(() => {
+ initMain(undefined, false, "?cly_device_id=abab", true);
+ expect(Countly.get_device_id_type()).to.equal(Countly.DeviceIdType.DEVELOPER_SUPPLIED);
+ expect(Countly.get_device_id()).to.eq("abab");
+ validateInternalDeviceIdType(DeviceIdTypeInternalEnumsTest.URL_PROVIDED);
+ Countly.begin_session();
+ cy.fetch_local_request_queue().then((eq) => {
+ checkRequestsForT(eq, DeviceIdTypeInternalEnumsTest.URL_PROVIDED);
+ });
+ });
+ });
+ it("69-SDK is initialized with custom device id, with offline mode, without utm device id", () => {
+ hp.haltAndClearStorage(() => {
+ initMain("customID", true, undefined, true);
+ expect(Countly.get_device_id_type()).to.equal(Countly.DeviceIdType.DEVELOPER_SUPPLIED);
+ expect(Countly.get_device_id()).to.eq("customID");
+ validateInternalDeviceIdType(DeviceIdTypeInternalEnumsTest.DEVELOPER_SUPPLIED);
+ Countly.begin_session();
+ cy.fetch_local_request_queue().then((eq) => {
+ checkRequestsForT(eq, DeviceIdTypeInternalEnumsTest.DEVELOPER_SUPPLIED);
+ });
+ });
+ });
+ it("70-SDK is initialized with custom device id, without offline mode, with utm device id", () => {
+ hp.haltAndClearStorage(() => {
+ initMain("customID2", false, "?cly_device_id=someID", true);
+ expect(Countly.get_device_id_type()).to.equal(Countly.DeviceIdType.DEVELOPER_SUPPLIED);
+ expect(Countly.get_device_id()).to.eq("someID");
+ validateInternalDeviceIdType(DeviceIdTypeInternalEnumsTest.URL_PROVIDED);
+ Countly.begin_session();
+ cy.fetch_local_request_queue().then((eq) => {
+ checkRequestsForT(eq, DeviceIdTypeInternalEnumsTest.URL_PROVIDED);
+ });
+ });
+ });
+ it("71-SDK is initialized without custom device id, with offline mode, with utm device id", () => {
+ hp.haltAndClearStorage(() => {
+ initMain(undefined, true, "?cly_device_id=someID", true);
+ expect(Countly.get_device_id_type()).to.equal(Countly.DeviceIdType.DEVELOPER_SUPPLIED);
+ expect(Countly.get_device_id()).to.eq("someID");
+ validateInternalDeviceIdType(DeviceIdTypeInternalEnumsTest.URL_PROVIDED);
+ Countly.begin_session();
+ cy.fetch_local_request_queue().then((eq) => {
+ checkRequestsForT(eq, DeviceIdTypeInternalEnumsTest.URL_PROVIDED);
+ });
+ });
+ });
+ it("72-SDK is initialized with custom device id, with offline mode, with utm device id", () => {
+ hp.haltAndClearStorage(() => {
+ initMain("customID3", true, "?cly_device_id=someID2", true);
+ expect(Countly.get_device_id_type()).to.equal(Countly.DeviceIdType.DEVELOPER_SUPPLIED);
+ expect(Countly.get_device_id()).to.eq("someID2");
+ validateInternalDeviceIdType(DeviceIdTypeInternalEnumsTest.URL_PROVIDED);
+ Countly.begin_session();
+ cy.fetch_local_request_queue().then((eq) => {
+ checkRequestsForT(eq, DeviceIdTypeInternalEnumsTest.URL_PROVIDED);
+ });
+ });
+ });
});
diff --git a/cypress/e2e/health_check.cy.js b/cypress/e2e/health_check.cy.js
index 131bb7d..2ccddf9 100644
--- a/cypress/e2e/health_check.cy.js
+++ b/cypress/e2e/health_check.cy.js
@@ -10,28 +10,42 @@ function initMain() {
});
}
-describe("Health Check tests ", () => {
+describe("Health Check tests", () => {
it("Check if health check is sent at the beginning", () => {
hp.haltAndClearStorage(() => {
initMain();
- cy.intercept("https://your.domain.count.ly/i?*").as("getXhr");
- cy.wait("@getXhr").then((xhr) => {
- const url = new URL(xhr.request.url);
+ cy.intercept("POST", "https://your.domain.count.ly/i").as("postXhr");
+ cy.wait("@postXhr").then((xhr) => {
+ const body = xhr.request.body;
+ const params = new URLSearchParams(body);
- // Test the 'hc' parameter
- const hcParam = url.searchParams.get("hc");
- const hcParamObj = JSON.parse(hcParam);
- expect(hcParamObj).to.eql({ el: 0, wl: 0, sc: -1, em: "\"\"" });
+ const hcParam = params.get("hc");
+ const hcParamObj = JSON.parse(decodeURIComponent(hcParam));
+ expect(hcParamObj).to.eql({ el: 0, wl: 0, sc: -1, em: "" });
- // Test the 'metrics' parameter
- const metricsParam = url.searchParams.get("metrics");
+ const metricsParam = params.get("metrics");
expect(metricsParam).to.equal("{\"_app_version\":\"0.0\",\"_ua\":\"abcd\"}");
- // check nothing in the request queue
cy.fetch_local_request_queue().then((rq) => {
expect(rq.length).to.equal(0);
});
});
});
});
+ it("Check no health check is sent in offline mode", () => {
+ hp.haltAndClearStorage(() => {
+ Countly.init({
+ app_key: "YOUR_APP_KEY",
+ url: "https://your.domain.count.ly",
+ test_mode: true,
+ offline_mode: true
+ });
+
+ cy.intercept("POST", "https://your.domain.count.ly/i").as("postXhr");
+ cy.get('@postXhr').should('not.exist');
+ cy.fetch_local_request_queue().then((rq) => {
+ expect(rq.length).to.equal(0);
+ });
+ });
+ });
});
diff --git a/cypress/e2e/remaining_requests.cy.js b/cypress/e2e/remaining_requests.cy.js
index 3382b9d..9c4af2d 100644
--- a/cypress/e2e/remaining_requests.cy.js
+++ b/cypress/e2e/remaining_requests.cy.js
@@ -18,7 +18,7 @@ describe("Remaining requests tests ", () => {
initMain(false);
// We will expect 4 requests: health check, begin_session, end_session, orientation
- hp.interceptAndCheckRequests(undefined, undefined, undefined, "?hc=*", "hc", (requestParams) => {
+ hp.interceptAndCheckRequests("POST", undefined, undefined, "?hc=*", "hc", (requestParams) => {
const params = JSON.parse(requestParams.get("hc"));
assert.isTrue(typeof params.el === "number");
assert.isTrue(typeof params.wl === "number");
@@ -29,19 +29,19 @@ describe("Remaining requests tests ", () => {
cy.wait(1000).then(() => {
// Create a session
Countly.begin_session();
- hp.interceptAndCheckRequests(undefined, undefined, undefined, "?begin_session=*", "begin_session", (requestParams) => {
+ hp.interceptAndCheckRequests("POST", undefined, undefined, "?begin_session=*", "begin_session", (requestParams) => {
expect(requestParams.get("begin_session")).to.equal("1");
expect(requestParams.get("rr")).to.equal("3");
expect(requestParams.get("av")).to.equal(av);
});
// End the session
Countly.end_session(undefined, true);
- hp.interceptAndCheckRequests(undefined, undefined, undefined, "?end_session=*", "end", (requestParams) => {
+ hp.interceptAndCheckRequests("POST", undefined, undefined, "?end_session=*", "end", (requestParams) => {
expect(requestParams.get("end_session")).to.equal("1");
expect(requestParams.get("rr")).to.equal("2");
expect(requestParams.get("av")).to.equal(av);
});
- hp.interceptAndCheckRequests(undefined, undefined, undefined, undefined, "orientation", (requestParams) => {
+ hp.interceptAndCheckRequests("POST", undefined, undefined, undefined, "orientation", (requestParams) => {
expect(JSON.parse(requestParams.get("events"))[0].key).to.equal("[CLY]_orientation");
expect(requestParams.get("rr")).to.equal("1");
expect(requestParams.get("av")).to.equal(av);
diff --git a/cypress/e2e/salt.cy.js b/cypress/e2e/salt.cy.js
new file mode 100644
index 0000000..05ac9a5
--- /dev/null
+++ b/cypress/e2e/salt.cy.js
@@ -0,0 +1,88 @@
+/* eslint-disable cypress/no-unnecessary-waiting */
+/* eslint-disable require-jsdoc */
+var Countly = require("../../Countly.js");
+var Utils = require("../../modules/Utils.js");
+// import * as Countly from "../../dist/countly_umd.js";
+var hp = require("../support/helper.js");
+const crypto = require('crypto');
+
+function initMain(salt) {
+ Countly.init({
+ app_key: "YOUR_APP_KEY",
+ url: "https://your.domain.count.ly",
+ debug: true,
+ salt: salt
+ });
+}
+const salt = "salt";
+
+/**
+* Tests for salt consists of:
+* 1. Init without salt
+* Create events and intercept the SDK requests. Request params should be normal and there should be no checksum
+* 2. Init with salt
+* Create events and intercept the SDK requests. Request params should be normal and there should be a checksum with length 64
+* 3. Node and Web Crypto comparison
+* Compare the checksums calculated by node crypto api and SDK's web crypto api for the same data. Should be equal
+*/
+describe("Salt Tests", () => {
+ it("Init without salt", () => {
+ hp.haltAndClearStorage(() => {
+ initMain(null);
+ var rqArray = [];
+ hp.events();
+ cy.intercept("GET", "**/i?**", (req) => {
+ const { url } = req;
+ rqArray.push(url.split("?")[1]); // get the query string
+ });
+ cy.wait(1000).then(() => {
+ cy.log(rqArray).then(() => {
+ for (const rq of rqArray) {
+ const paramsObject = hp.turnSearchStringToObject(rq);
+ hp.check_commons(paramsObject);
+ expect(paramsObject.checksum256).to.be.not.ok;
+ }
+ });
+ });
+ });
+ });
+ it("Init with salt", () => {
+ hp.haltAndClearStorage(() => {
+ initMain(salt);
+ var rqArray = [];
+ hp.events();
+ cy.intercept("GET", "**/i?**", (req) => {
+ const { url } = req;
+ rqArray.push(url.split("?")[1]);
+ });
+ cy.wait(1000).then(() => {
+ cy.log(rqArray).then(() => {
+ for (const rq of rqArray) {
+ const paramsObject = hp.turnSearchStringToObject(rq);
+ hp.check_commons(paramsObject);
+ expect(paramsObject.checksum256).to.be.ok;
+ expect(paramsObject.checksum256.length).to.equal(64);
+ // TODO: directly check the checksum with the node crypto api. Will need some extra decoding logic
+ }
+ });
+ });
+ });
+ });
+ it('Node and Web Crypto comparison', () => {
+ const hash = sha256("text" + salt); // node crypto api
+ Utils.calculateChecksum("text", salt).then((hash2) => { // SDK uses web crypto api
+ expect(hash2).to.equal(hash);
+ });
+ });
+});
+
+/**
+ * Calculate sha256 hash of given data
+ * @param {*} data - data to hash
+ * @returns {string} - sha256 hash
+ */
+function sha256(data) {
+ const hash = crypto.createHash('sha256');
+ hash.update(data);
+ return hash.digest('hex');
+}
\ No newline at end of file
diff --git a/cypress/e2e/utm.cy.js b/cypress/e2e/utm.cy.js
index 21c97d2..3a21175 100644
--- a/cypress/e2e/utm.cy.js
+++ b/cypress/e2e/utm.cy.js
@@ -19,6 +19,7 @@ describe("UTM tests ", () => {
it("Checks if a single default utm tag works", () => {
hp.haltAndClearStorage(() => {
initMulti("YOUR_APP_KEY", "?utm_source=hehe", undefined);
+ Countly.q.push(['track_errors']); // adding this as calling it during init used to cause an error (at v23.12.5)
cy.fetch_local_request_queue().then((rq) => {
cy.log(rq);
const custom = JSON.parse(rq[0].user_details).custom;
diff --git a/cypress/e2e/web_worker_requests.cy.js b/cypress/e2e/web_worker_requests.cy.js
index ad166a9..d9b9ae7 100644
--- a/cypress/e2e/web_worker_requests.cy.js
+++ b/cypress/e2e/web_worker_requests.cy.js
@@ -1,4 +1,4 @@
-import { appKey } from "../support/helper";
+import { turnSearchStringToObject, check_commons } from "../support/helper";
const myEvent = {
key: "buttonClick",
@@ -83,44 +83,3 @@ describe("Web Worker Request Intercepting Tests", () => {
});
});
});
-
-/**
- * Check common params for all requests
- * @param {Object} paramsObject - object from search string
- */
-function check_commons(paramsObject) {
- expect(paramsObject.timestamp).to.be.ok;
- expect(paramsObject.timestamp.toString().length).to.equal(13);
- expect(paramsObject.hour).to.be.within(0, 23);
- expect(paramsObject.dow).to.be.within(0, 7);
- expect(paramsObject.app_key).to.equal(appKey);
- expect(paramsObject.device_id).to.be.ok;
- expect(paramsObject.sdk_name).to.equal("javascript_native_web");
- expect(paramsObject.sdk_version).to.be.ok;
- expect(paramsObject.t).to.be.within(0, 3);
- expect(paramsObject.av).to.equal(0); // av is 0 as we parsed parsable things
- if (!paramsObject.hc) { // hc is direct request
- expect(paramsObject.rr).to.be.above(-1);
- }
- expect(paramsObject.metrics._ua).to.be.ok;
-}
-
-/**
- * Turn search string into object with values parsed
- * @param {String} searchString - search string
- * @returns {object} - object from search string
- */
-function turnSearchStringToObject(searchString) {
- const searchParams = new URLSearchParams(searchString);
- const paramsObject = {};
- for (const [key, value] of searchParams.entries()) {
- try {
- paramsObject[key] = JSON.parse(value); // try to parse value
- }
- catch (e) {
- paramsObject[key] = value;
- }
- }
- return paramsObject;
-}
-
diff --git a/cypress/support/helper.js b/cypress/support/helper.js
index a1b2baa..e38cc98 100644
--- a/cypress/support/helper.js
+++ b/cypress/support/helper.js
@@ -68,31 +68,43 @@ var waitFunction = function(startTime, waitTime, waitIncrement, continueCallback
};
/**
- * This intercepts the request the SDK makes and returns the request parameters to the callback function
- * @param {String} requestType - GET, POST, PUT, DELETE
- * @param {String} requestUrl - request url (https://your.domain.count.ly)
- * @param {String} endPoint - endpoint (/i)
- * @param {String} requestParams - request parameters (?begin_session=**)
+ * Intercepts SDK requests and returns request parameters to the callback function.
+ * @param {String} requestType - GET or POST
+ * @param {String} requestUrl - base URL (e.g., https://your.domain.count.ly)
+ * @param {String} endPoint - endpoint (e.g., /i)
+ * @param {String} aliasParam - parameter to match in requests (e.g., "hc", "begin_session")
* @param {String} alias - alias for the request
- * @param {Function} callback - callback function
+ * @param {Function} callback - callback function for parsed parameters
*/
-function interceptAndCheckRequests(requestType, requestUrl, endPoint, requestParams, alias, callback) {
+function interceptAndCheckRequests(requestType, requestUrl, endPoint, aliasParam, alias, callback) {
requestType = requestType || "GET";
- requestUrl = requestUrl || "https://your.domain.count.ly"; // TODO: might be needed in the future but not yet
+ requestUrl = requestUrl || "https://your.domain.count.ly";
endPoint = endPoint || "/i";
- requestParams = requestParams || "?*";
alias = alias || "getXhr";
- cy.intercept(requestUrl + endPoint + requestParams, (req) => {
- const { url } = req;
- req.reply(200, {result: "Success"}, {
+ // Intercept requests
+ cy.intercept(requestType, requestUrl + endPoint + "*", (req) => {
+ if (requestType === "POST" && req.body) {
+ // Parse URL-encoded body for POST requests
+ const params = new URLSearchParams(req.body);
+ callback(params);
+ } else {
+ // Parse URL parameters for GET requests
+ const url = new URL(req.url);
+ const params = url.searchParams;
+ callback(params);
+ }
+ req.reply(200, { result: "Success" }, {
"x-countly-rr": "2"
});
}).as(alias);
+
+ // Wait for the request alias to be triggered
cy.wait("@" + alias).then((xhr) => {
- const url = new URL(xhr.request.url);
- const searchParams = url.searchParams;
- callback(searchParams);
+ const params = requestType === "POST" && xhr.request.body
+ ? new URLSearchParams(xhr.request.body)
+ : new URL(xhr.request.url).searchParams;
+ callback(params);
});
}
@@ -274,6 +286,46 @@ function validateDefaultUtmTags(aq, source, medium, campaign, term, content) {
}
}
+/**
+ * Check common params for all requests
+ * @param {Object} paramsObject - object from search string
+ */
+function check_commons(paramsObject) {
+ expect(paramsObject.timestamp).to.be.ok;
+ expect(paramsObject.timestamp.toString().length).to.equal(13);
+ expect(paramsObject.hour).to.be.within(0, 23);
+ expect(paramsObject.dow).to.be.within(0, 7);
+ expect(paramsObject.app_key).to.equal(appKey);
+ expect(paramsObject.device_id).to.be.ok;
+ expect(paramsObject.sdk_name).to.equal("javascript_native_web");
+ expect(paramsObject.sdk_version).to.be.ok;
+ expect(paramsObject.t).to.be.within(0, 3);
+ expect(paramsObject.av).to.equal(0); // av is 0 as we parsed parsable things
+ if (!paramsObject.hc) { // hc is direct request
+ expect(paramsObject.rr).to.be.above(-1);
+ }
+ expect(paramsObject.metrics._ua).to.be.ok;
+}
+
+/**
+ * Turn search string into object with values parsed
+ * @param {String} searchString - search string
+ * @returns {object} - object from search string
+ */
+function turnSearchStringToObject(searchString) {
+ const searchParams = new URLSearchParams(searchString);
+ const paramsObject = {};
+ for (const [key, value] of searchParams.entries()) {
+ try {
+ paramsObject[key] = JSON.parse(decodeURIComponent(value)); // try to parse value
+ }
+ catch (e) {
+ paramsObject[key] = decodeURIComponent(value);
+ }
+ }
+ return paramsObject;
+}
+
module.exports = {
haltAndClearStorage,
sWait,
@@ -287,5 +339,7 @@ module.exports = {
testNormalFlow,
interceptAndCheckRequests,
validateDefaultUtmTags,
- userDetailObj
+ userDetailObj,
+ check_commons,
+ turnSearchStringToObject
};
\ No newline at end of file
diff --git a/examples/Angular/main.ts b/examples/Angular/main.ts
index 85da81a..5044a35 100644
--- a/examples/Angular/main.ts
+++ b/examples/Angular/main.ts
@@ -1,24 +1,25 @@
-import { enableProdMode } from '@angular/core';
-import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
-
-import { AppModule } from './app/app.module';
-import { environment } from './environments/environment';
+import { bootstrapApplication } from '@angular/platform-browser';
+import { appConfig } from './app/app.config';
+import { AppComponent } from './app/app.component';
import Countly from 'countly-sdk-web';
window.Countly = Countly;
+const COUNTLY_SERVER_KEY = "https://your.server.ly";
+const COUNTLY_APP_KEY = "YOUR_APP_KEY";
+
+if(COUNTLY_APP_KEY === "YOUR_APP_KEY" || COUNTLY_SERVER_KEY === "https://your.server.ly"){
+ console.warn("Please do not use default set of app key and server url")
+}
+// initializing countly with params
Countly.init({
- app_key: "YOUR_APP_KEY",
- url: "https://your.domain.count.ly",
+ app_key: COUNTLY_APP_KEY,
+ url: COUNTLY_SERVER_KEY, //your server goes here
debug: true
});
Countly.track_sessions();
-if (environment.production) {
- enableProdMode();
-
-}
+bootstrapApplication(AppComponent, appConfig)
+ .catch((err) => console.error(err));
-platformBrowserDynamic().bootstrapModule(AppModule)
- .catch(err => console.error(err));
diff --git a/examples/README.md b/examples/README.md
index ca75b37..7dce197 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -3,7 +3,7 @@
This folder is filled with examples of Countly implementations to help you to get an idea
how to integrate Countly into your own personal project too.
-For all projects you should change 'YOUR_APP_KEY' value with your own application's app key, 'https://your.domain.count.ly' value with your own server url.
+For all projects you should change 'YOUR_APP_KEY' value with your own application's app key, 'https://your.server.ly' value with your own server url.
If you have not separated 'examples' folder from the 'COUNTLY-SDK-WEB' project folder nor made some changes to file names in the periphery, any path to countly.js or the plugins must be still correct.
diff --git a/examples/React/src/index.js b/examples/React/src/index.js
index d5461ab..389a4f6 100644
--- a/examples/React/src/index.js
+++ b/examples/React/src/index.js
@@ -8,9 +8,17 @@ import Countly from 'countly-sdk-web';
//Exposing Countly to the DOM as a global variable
//Usecase - Heatmaps
window.Countly = Countly;
+
+const COUNTLY_SERVER_KEY = "https://your.server.ly";
+const COUNTLY_APP_KEY = "YOUR_APP_KEY";
+
+if(COUNTLY_APP_KEY === "YOUR_APP_KEY" || COUNTLY_SERVER_KEY === "https://your.server.ly"){
+ console.warn("Please do not use default set of app key and server url")
+}
+// initializing countly with params
Countly.init({
- app_key: 'YOUR_APP_KEY',
- url: 'YOUR_SERVER_URL',
+ app_key: COUNTLY_APP_KEY,
+ url: COUNTLY_SERVER_KEY, //your server goes here
debug: true
});
diff --git a/examples/Symbolication/src/main.js b/examples/Symbolication/src/main.js
index 39d8e79..a7033be 100644
--- a/examples/Symbolication/src/main.js
+++ b/examples/Symbolication/src/main.js
@@ -1,9 +1,16 @@
import Countly from "countly-sdk-web";
+const COUNTLY_SERVER_KEY = "https://your.server.ly";
+const COUNTLY_APP_KEY = "YOUR_APP_KEY";
+
+if(COUNTLY_APP_KEY === "YOUR_APP_KEY" || COUNTLY_SERVER_KEY === "https://your.server.ly"){
+ console.warn("Please do not use default set of app key and server url")
+}
+// initializing countly with params
Countly.init({
- app_key: "YOUR_APP_KEY",
+ app_key: COUNTLY_APP_KEY,
+ url: COUNTLY_SERVER_KEY, //your server goes here
app_version: "1.0",
- url: "https://your.domain.count.ly",
debug: true
});
diff --git a/examples/example_async.html b/examples/example_async.html
index ac68a00..bf312bd 100644
--- a/examples/example_async.html
+++ b/examples/example_async.html
@@ -12,7 +12,11 @@
//provide countly initialization parameters
Countly.app_key = "YOUR_APP_KEY";
- Countly.url = "https://your.domain.count.ly"; //your server goes here
+ Countly.url = "https://your.server.ly"; //your server goes here
+
+ if(Countly.app_key === "YOUR_APP_KEY" || Countly.url === "https://your.server.ly"){
+ console.warn("Please do not use default set of app key and server url")
+ }
Countly.debug = true;
//start pushing function calls to queue
diff --git a/examples/example_fb.html b/examples/example_fb.html
index 9edf033..12acdd8 100644
--- a/examples/example_fb.html
+++ b/examples/example_fb.html
@@ -11,10 +11,16 @@
diff --git a/examples/example_opt_out.html b/examples/example_opt_out.html
index 71ad8b2..c128a20 100644
--- a/examples/example_opt_out.html
+++ b/examples/example_opt_out.html
@@ -9,9 +9,16 @@
import Countly from '../Countly.js';
//initializing countly with params
+ const COUNTLY_SERVER_KEY = "https://your.server.ly";
+ const COUNTLY_APP_KEY = "YOUR_APP_KEY";
+
+ if(COUNTLY_APP_KEY === "YOUR_APP_KEY" || COUNTLY_SERVER_KEY === "https://your.server.ly"){
+ console.warn("Please do not use default set of app key and server url")
+ }
+ // initializing countly with params
Countly.init({
- app_key: "YOUR_APP_KEY",
- url: "https://your.domain.count.ly", //your server goes here
+ app_key: COUNTLY_APP_KEY,
+ url: COUNTLY_SERVER_KEY, //your server goes here
debug: true
})
//track sessions automatically
diff --git a/examples/example_rating_widgets.html b/examples/example_rating_widgets.html
index 955f64b..d9448c0 100644
--- a/examples/example_rating_widgets.html
+++ b/examples/example_rating_widgets.html
@@ -8,9 +8,16 @@