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 @@