diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index ff25f3e587a..002089dab61 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -392,3 +392,72 @@ jobs:
mkdir -p screenshots videos
tar zcvf "$ARTIFACT_ARCHIVE_NAME" screenshots videos
curl -o /tmp/uploader.log -u "${{ secrets.BOX_UPLOAD_AUTH }}" ${{ secrets.BOX_UPLOAD_PATH }} -T "$ARTIFACT_ARCHIVE_NAME"
+
+ ui-test-sdk:
+ runs-on: ubuntu-latest
+
+ services:
+ mongodb:
+ image: mongo:8.0
+ options: >-
+ --health-cmd mongosh
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+ ports:
+ - 27017:27017
+
+ container:
+ image: countly/countly-core:pipelines-${{ inputs.custom_tag || github.base_ref || github.ref_name }}
+ env:
+ COUNTLY_CONFIG__MONGODB_HOST: mongodb
+ COUNTLY_CONFIG_API_PREVENT_JOBS: true
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Install Chrome
+ shell: bash
+ run: |
+ apt update
+ apt install -y libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb wget
+ wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb -O /tmp/chrome.deb
+ apt install -y /tmp/chrome.deb
+
+ - name: Copy code
+ shell: bash
+ run: cp -rf ./* /opt/countly
+
+ - name: Prepare files to use correct MongoDB host
+ shell: bash
+ run: "sed -i 's/mongosh --quiet/mongosh --host mongodb --quiet/' /opt/countly/bin/backup/import_events.sh && sed -i 's/mongoimport --db/mongoimport --host mongodb --db/' /opt/countly/bin/backup/import_events.sh"
+
+ - name: NPM install
+ shell: bash
+ working-directory: /opt/countly
+ run: npm install
+
+ - name: Prepare environment
+ shell: bash
+ working-directory: /opt/countly
+ run: |
+ sed -i 's/port: 3001,/port: 3001, workers: 1,/' /opt/countly/api/config.js
+ cp "./plugins/plugins.default.json" "/opt/countly/plugins/plugins.json"
+ npm install
+ sudo countly task dist-all
+ bash bin/scripts/countly.prepare.ce.tests.sh
+ cd ui-tests
+ echo '{"username": "${{ secrets.CYPRESS_USER_USERNAME }}","email": "${{ secrets.CYPRESS_USER_EMAIL }}","password": "${{ secrets.CYPRESS_USER_PASSWORD }}"}' > cypress/fixtures/user.json
+ sed -i 's/00000000-0000-0000-0000-000000000000/${{ secrets.CYPRESS_KEY }}/g' package.json
+ cp cypress.config.sample.js cypress.config.js
+ sed -i 's/000000/${{ secrets.CYPRESS_PROJECT_ID }}/g' cypress.config.js
+
+ - name: Run UI tests
+ shell: bash
+ working-directory: /opt/countly
+ run: |
+ /sbin/my_init &
+ cd ui-tests
+ npm install
+ xvfb-run --auto-servernum --server-args="-screen 0 1280x1024x24" \
+ npm run cy:run:sdk
diff --git a/frontend/express/public/javascripts/countly/vue/components/helpers.js b/frontend/express/public/javascripts/countly/vue/components/helpers.js
index 5d33d3ce526..ee485222293 100644
--- a/frontend/express/public/javascripts/countly/vue/components/helpers.js
+++ b/frontend/express/public/javascripts/countly/vue/components/helpers.js
@@ -405,6 +405,10 @@
placement: {
type: String,
default: 'auto'
+ },
+ tooltipClass: {
+ type: String,
+ default: ''
}
},
computed: {
@@ -415,7 +419,7 @@
};
}
},
- template: ''
+ template: ''
}));
Vue.component("cly-remover", countlyBaseComponent.extend({
diff --git a/plugins/sdk/api/api.js b/plugins/sdk/api/api.js
index bf3a3a0c17c..bd3f095fecb 100644
--- a/plugins/sdk/api/api.js
+++ b/plugins/sdk/api/api.js
@@ -12,6 +12,49 @@ plugins.register("/permissions/features", function(ob) {
(function() {
+ /**
+ * @api {get} /o/sdk?method=sc Get SDK config
+ * @apiName GetSDKConfig
+ * @apiGroup SDK Config
+ * @apiPermission app
+ * @apiDescription Get SDK configuration for this SDK and this user
+ *
+ * @apiQuery {String} app_key Application key
+ *
+ * @apiSuccess {Object} v - version
+ * @apiSuccess {Object} t - timestamp
+ * @apiSuccess {Object} c - sdk config
+ *
+ * @apiSuccessExample {json} Success-Response:
+ * {
+ "v":1,
+ "t":1682328445330,
+ "c":{
+ "tracking":true,
+ "networking":true,
+ "crt":true,
+ "vt":true,
+ "st":true,
+ "cet":true,
+ "ecz":true,
+ "cr":true,
+ "sui":true,
+ "eqs":true,
+ "rqs":true,
+ "czi":true,
+ "dort":true,
+ "scui":true,
+ "lkl":true,
+ "lvs":true,
+ "lsv":true,
+ "lbc":true,
+ "ltlpt":true,
+ "ltl":true,
+ "lt":true,
+ "rcz":true
+ }
+ * }
+ */
plugins.register("/o/sdk", function(ob) {
var params = ob.params;
if (params.qstring.method !== "sc") {
@@ -36,36 +79,24 @@ plugins.register("/permissions/features", function(ob) {
});
/**
- * @api {get} /o?method=sc Get SDK config
- * @apiName GetSDKConfig
+ * @api {get} /o?method=config-upload Save SDK config
+ * @apiName SaveSDKConfig
* @apiGroup SDK Config
- * @apiPermission app
- * @apiDescription Get SDK configuration for this SDK and this user
- *
- * @apiQuery {String} app_key Application key
- *
- * @apiSuccess {Object} v - version
- * @apiSuccess {Object} t - timestamp
- * @apiSuccess {Object} c - sdk config
- *
- * @apiSuccessExample {json} Success-Response:
+ * @apiPermission admin
+ * @apiDescription Save SDK configuration for the given app
+ *
+ * @apiQuery {String} app_id Application ID
+ * @apiQuery {String} config SDK config object
+ *
+ * @apiSuccess {json} Success-Response:
* {
- "v":1,
- "t":1682328445330,
- "c":{
- "tracking":false,
- "networking":false,
- "crashes":false,
- "views":false,
- "heartbeat":61,
- "event_queue":11,
- "request_queue":1001
- }
+ * "result": "Success"
* }
*/
plugins.register("/o", function(ob) {
var params = ob.params;
+ // returns server config for the given app
if (params.qstring.method === "sdk-config") {
validateRead(params, FEATURE_NAME, function() {
getSDKConfig(params).then(function(res) {
@@ -78,6 +109,77 @@ plugins.register("/permissions/features", function(ob) {
return true;
}
+
+ // saves the given server configuration for the given app
+ if (params.qstring.method === "config-upload") {
+ return new Promise(function(resolve) {
+ validateUpdate(params, FEATURE_NAME, function() {
+ var uploadConfig = params.qstring.config;
+ if (uploadConfig && typeof uploadConfig === "string") {
+ try {
+ uploadConfig = JSON.parse(uploadConfig);
+ }
+ catch (ex) {
+ common.returnMessage(params, 400, 'Invalid config format');
+ return resolve();
+ }
+ }
+
+ if (!uploadConfig || typeof uploadConfig !== "object") {
+ common.returnMessage(params, 400, 'Config must be a valid object');
+ return resolve();
+ }
+
+ var configToSave = uploadConfig.c || uploadConfig; // incase they provide the config object directly
+ var validOptions = [
+ "tracking",
+ "networking",
+ "crt",
+ "vt",
+ "st",
+ "cet",
+ "ecz",
+ "cr",
+ "sui",
+ "eqs",
+ "rqs",
+ "czi",
+ "dort",
+ "scui",
+ "lkl",
+ "lvs",
+ "lsv",
+ "lbc",
+ "ltlpt",
+ "ltl",
+ "lt",
+ "rcz"
+ ];
+ for (var key in configToSave) {
+ if (validOptions.indexOf(key) === -1) {
+ delete configToSave[key];
+ }
+ }
+
+ common.outDb.collection('sdk_configs').updateOne(
+ {_id: params.qstring.app_id + ""},
+ {$set: {config: configToSave}},
+ {upsert: true},
+ function(err) {
+ if (err) {
+ common.returnMessage(params, 500, 'Error saving config to database');
+ }
+ else {
+ common.returnOutput(params, {result: 'Success'});
+ }
+ resolve();
+ }
+ );
+ });
+ });
+ }
+
+ return false;
});
plugins.register("/i/sdk-config", function(ob) {
@@ -359,7 +461,7 @@ plugins.register("/permissions/features", function(ob) {
});
/**
- * Updated SDK config
+ * Updates SDK config (used internally when configuration is changed in the dashboard)
* @param {params} params - request params
* @returns {void}
*/
diff --git a/plugins/sdk/frontend/public/javascripts/countly.views.js b/plugins/sdk/frontend/public/javascripts/countly.views.js
index 865853634c9..c1bf80bacba 100644
--- a/plugins/sdk/frontend/public/javascripts/countly.views.js
+++ b/plugins/sdk/frontend/public/javascripts/countly.views.js
@@ -1,5 +1,37 @@
-/*global app, countlyVue, countlySDK, CV, countlyCommon*/
+/*global app, countlyVue, countlySDK, CV, countlyCommon, CountlyHelpers*/
(function() {
+ var SC_VER = 1; // check/update sdk/api/api.js for this
+ var v0_android = "22.09.4";
+ var v0_ios = "23.02.2";
+ var v1_android = "25.4.0";
+ var v1_ios = "25.4.0";
+ var v1_web = "25.4.0";
+ // Supporting SDK Versions for the SC options
+ var supportedSDKVersion = {
+ tracking: { android: v0_android, ios: v0_ios, web: v1_web },
+ networking: { android: v0_android, ios: v0_ios, web: v1_web },
+ crt: { android: v1_android, ios: v1_ios, web: v1_web },
+ vt: { android: v1_android, ios: v1_ios, web: v1_web },
+ st: { android: v1_android, ios: v1_ios, web: v1_web },
+ cet: { android: v1_android, ios: v1_ios, web: v1_web },
+ ecz: { android: v1_android, ios: v1_ios, web: v1_web },
+ cr: { android: v1_android, ios: v1_ios, web: v1_web },
+ sui: { android: v1_android, ios: v1_ios, web: v1_web },
+ eqs: { android: v1_android, ios: v1_ios, web: v1_web },
+ rqs: { android: v1_android, ios: v1_ios, web: v1_web },
+ czi: { android: v1_android, ios: v1_ios, web: v1_web },
+ dort: { android: v1_android, ios: v1_ios, web: v1_web },
+ scui: { android: v1_android, ios: v1_ios, web: v1_web },
+ lkl: { android: v1_android, ios: v1_ios, web: v1_web },
+ lvs: { android: v1_android, ios: v1_ios, web: v1_web },
+ lsv: { android: v1_android, ios: v1_ios, web: v1_web },
+ lbc: { android: v1_android, ios: v1_ios, web: v1_web },
+ ltlpt: { android: v1_android, ios: v1_ios, web: v1_web },
+ ltl: { android: v1_android, ios: v1_ios, web: v1_web },
+ lt: { android: v1_android, ios: v1_ios, web: v1_web },
+ rcz: { android: v1_android, ios: v1_ios, web: v1_web }
+ };
+
var FEATURE_NAME = "sdk";
var SDK = countlyVue.views.create({
template: CV.T('/sdk/templates/sdk-main.html'),
@@ -49,7 +81,10 @@
template: CV.T('/sdk/templates/config.html'),
created: function() {
var self = this;
- this.$store.dispatch("countlySDK/initialize").then(function() {
+ Promise.all([
+ this.$store.dispatch("countlySDK/initialize"),
+ this.$store.dispatch("countlySDK/fetchSDKStats") // fetch sdk version data for tooltips
+ ]).then(function() {
self.$store.dispatch("countlySDK/sdk/setTableLoading", false);
});
},
@@ -77,7 +112,7 @@
},
features: {
label: "SDK Features",
- list: ["crt", "vt", "st", "cet", "ecz", "cr"]
+ list: ["crt", "vt", "st", "cet", "lt", "ecz", "cr", "rcz"]
},
settings: {
label: "SDK Settings",
@@ -99,7 +134,7 @@
networking: {
type: "switch",
name: "Allow Networking",
- description: "Enable or disable all networking calls from SDK except SDK config call. Does not effect tracking of data (default: enabled)",
+ description: "Enable or disable all networking calls from SDK except SDK behavior call. Does not effect tracking of data (default: enabled)",
default: true,
value: null
},
@@ -138,17 +173,24 @@
default: true,
value: null
},
+ lt: {
+ type: "switch",
+ name: "Allow Location Tracking",
+ description: "Enable or disable tracking of location (default: enabled)",
+ default: true,
+ value: null
+ },
ecz: {
type: "switch",
name: "Enable Content Zone",
- description: "Enable or disable listening to Journey related contents (default: false)",
+ description: "Enable or disable listening to Journey related contents (default: disabled)",
default: false,
value: null
},
cr: {
type: "switch",
name: "Require Consent",
- description: "Enable or disable requiring consent for tracking (default: false)",
+ description: "Enable or disable requiring consent for tracking (default: disabled)",
default: false,
value: null
},
@@ -176,14 +218,14 @@
dort: {
type: "number",
name: "Request Drop Age",
- description: "Provide time in hours after which an old request should be dropped if they are not sent to server (default: 0 = not enabled)",
+ description: "Provide time in hours after which an old request should be dropped if they are not sent to server (default: 0 = disabled)",
default: 0,
value: null
},
lkl: {
type: "number",
name: "Max Key Length",
- description: "Maximum length of an Event's key (including name) (default: 128)",
+ description: "Maximum length of Event and segment keys (including name) (default: 128)",
default: 128,
value: null
},
@@ -224,17 +266,29 @@
},
scui: {
type: "number",
- name: "Server Config Update Interval",
- description: "How often to check for new server config in hours (default: 4)",
+ name: "SDK Behavior Update Interval",
+ description: "How often to check for new behavior settings in hours (default: 4)",
default: 4,
value: null
+ },
+ rcz: {
+ type: "switch",
+ name: "Allow Refresh Content Zone",
+ description: "Enable or disable refreshing Journey content (default: enabled)",
+ default: true,
+ value: null
}
},
diff: [],
- description: "This is experimental feature and not all SDKs and SDK versions yet support it. Refer to the SDK documentation for more information",
- downloadDescription: "Download the current SDK configuration as a JSON file to provide to the SDK",
+ description: "Not all SDKs and SDK versions yet support this feature. Refer to respective SDK documentation for more information"
};
},
+ mounted: function() {
+ var self = this;
+ this.$nextTick(function() {
+ self.checkSdkSupport();
+ });
+ },
methods: {
onChange: function(key, value) {
this.configs[key].value = value;
@@ -257,7 +311,7 @@
downloadConfig: function() {
var params = this.$store.getters["countlySDK/sdk/all"];
var data = {};
- data.v = 1; // check sdk/api/api.js for version
+ data.v = SC_VER;
data.t = Date.now();
data.c = params || {};
var configData = JSON.stringify(data, null, 2);
@@ -271,6 +325,27 @@
document.body.removeChild(a);
URL.revokeObjectURL(url);
},
+ resetSDKConfiguration: function() {
+ var helper_msg = "You are about to reset your SDK behavior to default state. This would override all these settings if set in your SDK. Do you want to continue?";
+ var helper_title = "Reset Behavior?";
+ var self = this;
+
+ CountlyHelpers.confirm(helper_msg, "red", function(result) {
+ if (!result) {
+ return true;
+ }
+
+ var params = self.$store.getters["countlySDK/sdk/all"];
+ var data = params || {};
+ for (var key in self.configs) {
+ self.configs[key].value = self.configs[key].default;
+ data[key] = self.configs[key].value;
+ }
+ self.$store.dispatch("countlySDK/sdk/update", data).then(function() {
+ self.$store.dispatch("countlySDK/initialize");
+ });
+ }, ["No, don't reset", "Yes, reset"], {title: helper_title});
+ },
save: function() {
var params = this.$store.getters["countlySDK/sdk/all"];
var data = params || {};
@@ -290,6 +365,120 @@
for (var key in this.configs) {
this.configs[key].value = typeof data[key] !== "undefined" ? data[key] : this.configs[key].default;
}
+ },
+ semverToNumber: function(version) {
+ if (typeof version !== 'string') {
+ return -1;
+ }
+
+ version = version.split("-")[0];
+ var letterIndex = version.search(/[a-zA-Z]/);
+ if (letterIndex !== -1) {
+ version = version.substring(0, letterIndex);
+ }
+
+ const semverRegex = /^(\d+)\.(\d+)\.(\d+)$/;
+ const match = version.match(semverRegex);
+
+ if (!match) {
+ return -1;
+ }
+
+ const major = parseInt(match[1], 10);
+ const minor = parseInt(match[2], 10);
+ const patch = parseInt(match[3], 10);
+
+ return major * 1000000 + minor * 1000 + patch;
+ },
+ compareVersions: function(context, a, b, text) {
+ if (!a) {
+ return;
+ }
+
+ const aValue = this.semverToNumber(a);
+ const bValue = this.semverToNumber(b);
+
+ if (aValue === -1 || bValue === -1) {
+ context.unsupportedList.push(text);
+ return;
+ }
+
+ if (aValue >= bValue) {
+ context.supportLevel += 1;
+ }
+ else {
+ context.unsupportedList.push(text);
+ }
+ },
+ checkSdkSupport: function() {
+ for (var key in this.configs) {
+ this.configs[key].tooltipMessage = "No SDK data present. Please use the latest versions of Android, Web, iOS, Flutter or RN SDKs to use this option.";
+ this.configs[key].tooltipClass = 'tooltip-neutral';
+ }
+
+ if (!this.$store.state.countlySDK ||
+ !this.$store.state.countlySDK.stats ||
+ !this.$store.state.countlySDK.stats.sdk ||
+ !this.$store.state.countlySDK.stats.sdk.versions ||
+ this.$store.state.countlySDK.stats.sdk.versions.length === 0) {
+ setTimeout(() => {
+ this.checkSdkSupport();
+ }, 500);
+ return;
+ }
+
+ const availableData = this.$store.state.countlySDK.stats.sdk.versions;
+ const latestVersions = availableData.reduce((acc, sdk) => {
+ if (!sdk.data || sdk.data.length === 0) {
+ return acc;
+ }
+ acc[sdk.label] = sdk.data[0].sdk_version;
+ for (var i = 1; i < sdk.data.length; i++) {
+ if (this.semverToNumber(acc[sdk.label]) < this.semverToNumber(sdk.data[i].sdk_version)) {
+ acc[sdk.label] = sdk.data[i].sdk_version;
+ }
+ }
+ return acc;
+ }, {});
+
+ var viableSDKCount = 0;
+ if (latestVersions.javascript_native_web) {
+ viableSDKCount++;
+ }
+ if (latestVersions["java-native-android"]) {
+ viableSDKCount++;
+ }
+ if (latestVersions["objc-native-ios"]) {
+ viableSDKCount++;
+ }
+
+ const configKeyList = Object.keys(this.configs);
+ configKeyList.forEach(configKey => {
+ const configSupportedVersions = supportedSDKVersion[configKey];
+ if (!configSupportedVersions) {
+ return;
+ }
+
+ var context = { supportLevel: 0, unsupportedList: [] };
+ this.compareVersions(context, latestVersions.javascript_native_web, configSupportedVersions.web, "Web SDK");
+ this.compareVersions(context, latestVersions["java-native-android"], configSupportedVersions.android, "Android SDK");
+ this.compareVersions(context, latestVersions["objc-native-ios"], configSupportedVersions.ios, "iOS SDK");
+
+ if (viableSDKCount > 0 && context.supportLevel === viableSDKCount) { // all correct version
+ this.configs[configKey].tooltipMessage = 'You are using SDKs that support this option.';
+ this.configs[configKey].tooltipClass = 'tooltip-success';
+ }
+ else if (context.unsupportedList.length > 0) { // some/all wrong version
+ this.configs[configKey].tooltipMessage = 'Some SDKs you use do not support this option: ' + context.unsupportedList.join(', ') + '. Try upgrading to the latest version.';
+ this.configs[configKey].tooltipClass = 'tooltip-warning';
+ }
+ else { // none supported
+ this.configs[configKey].tooltipMessage = 'None of the SDKs you use support this option. Please use the latest versions of Android, Web, iOS, Flutter or RN SDKs to use this option.';
+ this.configs[configKey].tooltipClass = 'tooltip-danger';
+ }
+
+ });
+ this.$forceUpdate();
}
}
});
@@ -297,7 +486,7 @@
priority: 2,
route: "#/manage/sdk/configurations",
component: SDKConfigurationView,
- title: "SDK Configuration",
+ title: "SDK Behavior Settings",
name: "configurations",
permission: FEATURE_NAME,
vuex: [
@@ -747,4 +936,5 @@
permission: FEATURE_NAME,
vuex: []
});
+
})();
\ No newline at end of file
diff --git a/plugins/sdk/frontend/public/localization/sdk.properties b/plugins/sdk/frontend/public/localization/sdk.properties
index 505879c78ea..8b71fd2f9b6 100644
--- a/plugins/sdk/frontend/public/localization/sdk.properties
+++ b/plugins/sdk/frontend/public/localization/sdk.properties
@@ -1,4 +1,4 @@
#sdk
sdk.plugin-title = SDK Manager
-sdk.plugin-description = SDK configuration and data
+sdk.plugin-description = SDK behavior and data
diff --git a/plugins/sdk/frontend/public/stylesheets/main.scss b/plugins/sdk/frontend/public/stylesheets/main.scss
index 247756f0c6d..7c68c43777e 100644
--- a/plugins/sdk/frontend/public/stylesheets/main.scss
+++ b/plugins/sdk/frontend/public/stylesheets/main.scss
@@ -71,3 +71,19 @@
margin-left: 8px;
}
}
+
+.cly-vue-tooltip-icon.ion.ion-help-circled.tooltip-success {
+ color: #6c3 !important;
+}
+
+.cly-vue-tooltip-icon.ion.ion-help-circled.tooltip-warning {
+ color: #fc0 !important;
+}
+
+.cly-vue-tooltip-icon.ion.ion-help-circled.tooltip-danger {
+ color: #f55 !important;
+}
+
+.cly-vue-tooltip-icon.ion.ion-help-circled.tooltip-neutral {
+ color: #A7AEB8 !important;
+}
\ No newline at end of file
diff --git a/plugins/sdk/frontend/public/templates/config.html b/plugins/sdk/frontend/public/templates/config.html
index 6b2ca770287..41c3ffb8bde 100644
--- a/plugins/sdk/frontend/public/templates/config.html
+++ b/plugins/sdk/frontend/public/templates/config.html
@@ -1,8 +1,13 @@
+
+
+ Reset configuration
+
+
Download Config
@@ -25,7 +30,14 @@
v-for="key in group.list" :key="key"
class="bu-columns bu-is-vcentered bu-p-5 config-section">
-
{{getData[key].name}}
+
+ {{getData[key].name}}
+
+
+
diff --git a/plugins/sdk/frontend/public/templates/request_stats.html b/plugins/sdk/frontend/public/templates/request_stats.html
index 6459e4bc797..e613fe36c87 100644
--- a/plugins/sdk/frontend/public/templates/request_stats.html
+++ b/plugins/sdk/frontend/public/templates/request_stats.html
@@ -1,5 +1,5 @@
-
+
diff --git a/plugins/sdk/frontend/public/templates/stats.html b/plugins/sdk/frontend/public/templates/stats.html
index c7455a01441..3e277953e3d 100644
--- a/plugins/sdk/frontend/public/templates/stats.html
+++ b/plugins/sdk/frontend/public/templates/stats.html
@@ -1,6 +1,6 @@
diff --git a/plugins/sdk/tests.js b/plugins/sdk/tests.js
index e69de29bb2d..6d7ba78c841 100644
--- a/plugins/sdk/tests.js
+++ b/plugins/sdk/tests.js
@@ -0,0 +1,268 @@
+const spt = require('supertest');
+const should = require('should');
+const testUtils = require('../../test/testUtils');
+const plugins = require('../../plugins/pluginManager');
+
+const request = spt(testUtils.url);
+// change these in local testing directly or set env vars (also COUNTLY_CONFIG_HOSTNAME should be set with port)
+let API_KEY_ADMIN = testUtils.get("API_KEY_ADMIN");
+let APP_KEY = testUtils.get('APP_KEY');
+let APP_ID = testUtils.get("APP_ID");
+const config_ver = 1;
+
+describe('SDK Plugin', function() {
+ //==================================================================================================================
+ // method=sc tests
+ //==================================================================================================================
+ describe('GET /o/sdk?method=sc', function() {
+ it('1. should get SDK config', function(done) {
+ request
+ .get('/o/sdk')
+ .query({ method: 'sc', app_key: APP_KEY, device_id: 'test' })
+ .expect(200)
+ .end(function(err, res) {
+ should.not.exist(err);
+ checkCommonConfigParam(res);
+ done();
+ });
+ });
+
+ checkBadCredentials('/o/sdk', 'sc', true);
+ });
+
+ //==================================================================================================================
+ // method=sdk-config tests
+ //==================================================================================================================
+ describe('GET /o?method=sdk-config', function() {
+ it('1. should get SDK config for admin', function(done) {
+ request
+ .get('/o')
+ .query({ method: 'sdk-config', api_key: API_KEY_ADMIN, app_id: APP_ID })
+ .expect(200)
+ .end(function(err, res) {
+ should.not.exist(err);
+ res.body.should.be.an.Object();
+ done();
+ });
+ });
+
+ checkBadCredentials('/o', 'sdk-config');
+ });
+
+ //==================================================================================================================
+ // /i/sdk-config/update-parameter tests
+ //==================================================================================================================
+ describe('POST /i/sdk-config/update-parameter', function() {
+ it('1. should update SDK parameter', function(done) {
+ const parameter = {
+ tracking: true,
+ networking: true,
+ crt: true
+ };
+
+ request
+ .post('/i/sdk-config/update-parameter')
+ .send({
+ api_key: API_KEY_ADMIN,
+ app_id: APP_ID,
+ parameter: JSON.stringify(parameter)
+ })
+ .expect(200)
+ .end(function(err, res) {
+ should.not.exist(err);
+ res.body.should.have.property('result', 'Success');
+ done();
+ });
+ });
+
+ // TODO: This seems to only need app_id and not api_key (so tests 5 and 6 fails), check if that is fine (disabling them for now)
+ checkBadCredentials('/i/sdk-config/update-parameter', 'sdk-config', false, true);
+
+ it('7. should validate parameter format', function(done) {
+ request
+ .post('/i/sdk-config/update-parameter')
+ .send({
+ api_key: API_KEY_ADMIN,
+ app_id: APP_ID,
+ parameter: 'invalid json'
+ })
+ .expect(400)
+ .end(function(err, res) {
+ should.not.exist(err);
+ res.body.should.have.property('result', 'Error parsing parameter');
+ done();
+ });
+ });
+ });
+
+ //==================================================================================================================
+ // method=sdks tests
+ //==================================================================================================================
+ describe('GET /o?method=sdks', function() {
+ it('1. should get SDK stats', function(done) {
+ request
+ .get('/o')
+ .query({ method: 'sdks', api_key: API_KEY_ADMIN, app_id: APP_ID })
+ .expect(200)
+ .end(function(err, res) {
+ should.not.exist(err);
+ res.body.should.be.an.Object();
+ done();
+ });
+ });
+
+ checkBadCredentials('/o', 'sdks');
+ });
+
+ //==================================================================================================================
+ // method=config-upload tests
+ //==================================================================================================================
+ describe('GET /o?method=config-upload', function() {
+ it('1. uploads config', function(done) {
+ request
+ .get('/o')
+ .query({ method: 'config-upload', api_key: API_KEY_ADMIN, app_id: APP_ID, config: JSON.stringify({}) })
+ .expect(200)
+ .end(function(err, res) {
+ should.not.exist(err);
+ res.body.should.be.an.Object();
+ res.body.should.have.property('result', 'Success');
+ done();
+ });
+ });
+
+ checkBadCredentials('/o', 'config-upload');
+
+ it('7. should reject invalid config format', function(done) {
+ request
+ .get('/o')
+ .query({ method: 'config-upload', api_key: API_KEY_ADMIN, app_id: APP_ID, config: 'invalid json' })
+ .expect(400)
+ .end(function(err, res) {
+ should.not.exist(err);
+ res.body.should.have.property('result', 'Invalid config format');
+ done();
+ });
+ });
+
+ it('8. should update correct config parameter', function(done) {
+ request
+ .get('/o')
+ .query({ method: 'config-upload', api_key: API_KEY_ADMIN, app_id: APP_ID, config: JSON.stringify({ lt: 500}) })
+ .expect(200)
+ .end(function(err, res) {
+ should.not.exist(err);
+ res.body.should.have.property('result', 'Success');
+ request
+ .get('/o/sdk')
+ .query({ method: 'sc', app_key: APP_KEY, device_id: 'test' })
+ .expect(200)
+ .end(function(err, res) {
+ should.not.exist(err);
+ checkCommonConfigParam(res);
+ res.body.c.lt.should.be.exactly(500);
+ done();
+ });
+ });
+ });
+
+ it('9. should omit invalid config parameter', function(done) {
+ request
+ .get('/o')
+ .query({ method: 'config-upload', api_key: API_KEY_ADMIN, app_id: APP_ID, config: JSON.stringify({ garbage: 500 }) })
+ .expect(200)
+ .end(function(err, res) {
+ should.not.exist(err);
+ res.body.should.have.property('result', 'Success');
+ request
+ .get('/o/sdk')
+ .query({ method: 'sc', app_key: APP_KEY, device_id: 'test' })
+ .expect(200)
+ .end(function(err, res) {
+ should.not.exist(err);
+ checkCommonConfigParam(res);
+ res.body.c.should.not.have.property('garbage');
+ done();
+ });
+ });
+ });
+
+ });
+});
+
+/**
+ * Check common server configuration parameters in the response.
+ * @param {*} res - The response object to check.
+ */
+function checkCommonConfigParam(res) {
+ res.body.should.have.property('v');
+ res.body.v.should.be.exactly(config_ver);
+ res.body.should.have.property('t');
+ res.body.t.should.be.a.Number();
+ res.body.t.toString().length.should.be.exactly(13);
+ res.body.should.have.property('c');
+ res.body.c.should.be.an.Object();
+}
+
+/**
+ * Check bad credentials for a given endpoint and method (for api_key and app_id requiring endpoints)
+ * @param {string} endpoint - endpoint to test like '/o'
+ * @param {string} method - method to test like 'sdk-config'
+ * @param {bool} userType - set to true if endpoint checks for app_key and device_id
+ * @param {bool} usePost - set to true if the method requires POST request
+ *
+ * Basically goes through these steps depending on userType value:
+ * 2. Provide invalid api_key OR Provide invalid app_key
+ * 3. Provide no api_key OR Provide no app_key
+ * 4. Provide invalid app_id OR Provide no device_id
+ * 5. Provide no app_id OR Provide no app_key and device_id
+ * 6. Provide no app_id and no api_key OR -
+ */
+function checkBadCredentials(endpoint, method, userType, usePost) {
+ var titles = ['2. should require valid api_key', '3. should require api_key', '4. should require valid app_id', '5. should require app_id', '6. should require app_id or api_key'];
+ var queries = [
+ { method: method, api_key: 'invalid_key', app_id: APP_ID },
+ { method: method, app_id: APP_ID },
+ { method: method, api_key: API_KEY_ADMIN, app_id: 'invalid_app_id' },
+ { method: method, api_key: API_KEY_ADMIN },
+ {method: method}
+ ];
+ var responses = ['User does not exist', 'Missing parameter "api_key" or "auth_token"', 'Invalid parameter "app_id"', 'Missing parameter "app_id"', 'Missing parameter "app_id"'];
+
+ // for app_key and device_id requiring endpoints
+ if (userType) {
+ titles = ['2. should require valid app_key', '3. should require app_key', '4. should require device_id', '5. should require app_key and device_id'];
+ queries = [
+ { method: method, app_key: 'invalid_key', device_id: 'test' },
+ { method: method, device_id: 'test' },
+ { method: method, app_key: APP_KEY },
+ { method: method }
+ ];
+ responses = ['App does not exist', 'Missing parameter "app_key" or "device_id"', 'Missing parameter "app_key" or "device_id"', 'Missing parameter "app_key" or "device_id"'];
+ }
+
+ // looping through all the test cases
+ for (var i = 0; i < titles.length; i++) {
+ (function(index) {
+ it(titles[index], function(done) {
+ let req = request
+ .get(endpoint)
+ .query(queries[index]);
+ if (usePost) {
+ if (index === 3 || index === 4) {
+ return done();
+ }
+ req = request
+ .post(endpoint)
+ .send(queries[index]);
+ }
+ req.expect(responses[index] === 'User does not exist' ? 401 : 400)
+ .end(function(err, res) {
+ should.not.exist(err);
+ res.body.result.should.be.exactly(responses[index]);
+ done();
+ });
+ });
+ })(i);
+ }
+}
\ No newline at end of file
diff --git a/ui-tests/cypress/e2e/sdk/notes.md b/ui-tests/cypress/e2e/sdk/notes.md
new file mode 100644
index 00000000000..799786a9186
--- /dev/null
+++ b/ui-tests/cypress/e2e/sdk/notes.md
@@ -0,0 +1,25 @@
+# SDK Plugin Test Notes
+
+## Server Config Tooltip Tests
+
+- Structure of tests:
+- 1. Neutral tooltip (default in app creation)
+- 2. Warning tooltip (old Web SDK version)
+- 3. Success tooltip (latest Web SDK version)
+- 4. Mixed tooltip (old Android SDK version)
+- 5. Mixed tooltip (old iOS SDK version)
+- 6. Danger tooltip (unsupported SDK)
+- 7. Success tooltip (latest Android SDK version)
+- 8. Success tooltip (latest iOS SDK version)
+- 9. Mixed tooltip (multiple SDK versions)
+
+- Tests are not checking tooltip text. This should be added.
+- No RN and Flutter tests yet. They should be added.
+- There can be more tests with bad conditions (like weird sdk versions).
+- For local tests you might want to change this command:
+
+```javascript
+Cypress.Commands.add('dropMongoDatabase', ({ local }) => {
+ cy.exec(`mongosh ${local ? 'mongodb' : 'localhost'}/countly --eval 'db.dropDatabase()'`);
+});
+```
diff --git a/ui-tests/cypress/e2e/sdk/tool_01.cy.js b/ui-tests/cypress/e2e/sdk/tool_01.cy.js
new file mode 100644
index 00000000000..f06249b1281
--- /dev/null
+++ b/ui-tests/cypress/e2e/sdk/tool_01.cy.js
@@ -0,0 +1,14 @@
+import { setupTest, goToConfigTab, checkTooltipAppears } from "../../lib/sdk/setup";
+
+describe('1.Neutral tooltip (default at app creation)', () => {
+ it('1.1-Setup', function() {
+ setupTest();
+ });
+ it('1.2-Reset', function() {
+ goToConfigTab();
+ });
+ it('1.3-Test', function() {
+ goToConfigTab(true);
+ checkTooltipAppears('neutral');
+ });
+});
\ No newline at end of file
diff --git a/ui-tests/cypress/e2e/sdk/tool_02.cy.js b/ui-tests/cypress/e2e/sdk/tool_02.cy.js
new file mode 100644
index 00000000000..9a07d313f13
--- /dev/null
+++ b/ui-tests/cypress/e2e/sdk/tool_02.cy.js
@@ -0,0 +1,19 @@
+import { setupTest, goToConfigTab, checkTooltipAppears, createRequest } from "../../lib/sdk/setup";
+
+describe('2.Warning tooltip (old Web SDK version)', () => {
+ it('2.1-Setup', function() {
+ setupTest();
+ cy.request('GET', createRequest('javascript_native_web', '19.12.1'))
+ .then((response) => {
+ // eslint-disable-next-line no-undef
+ expect(response.status).to.eq(200);
+ });
+ });
+ it('2.2-Reset', function() {
+ goToConfigTab();
+ });
+ it('2.3-Test', function() {
+ goToConfigTab(true);
+ checkTooltipAppears('warning');
+ });
+});
\ No newline at end of file
diff --git a/ui-tests/cypress/e2e/sdk/tool_03.cy.js b/ui-tests/cypress/e2e/sdk/tool_03.cy.js
new file mode 100644
index 00000000000..da8a9723c84
--- /dev/null
+++ b/ui-tests/cypress/e2e/sdk/tool_03.cy.js
@@ -0,0 +1,19 @@
+import { setupTest, goToConfigTab, checkTooltipAppears, createRequest } from "../../lib/sdk/setup";
+
+describe('3.Success tooltip (latest Web SDK version)', () => {
+ it('3.1-Setup', function() {
+ setupTest();
+ cy.request('GET', createRequest('javascript_native_web', '25.12.1'))
+ .then((response) => {
+ // eslint-disable-next-line no-undef
+ expect(response.status).to.eq(200);
+ });
+ });
+ it('3.2-Reset', function() {
+ goToConfigTab();
+ });
+ it('3.3-Test', function() {
+ goToConfigTab(true);
+ checkTooltipAppears('success');
+ });
+});
\ No newline at end of file
diff --git a/ui-tests/cypress/e2e/sdk/tool_04.cy.js b/ui-tests/cypress/e2e/sdk/tool_04.cy.js
new file mode 100644
index 00000000000..4ef51102515
--- /dev/null
+++ b/ui-tests/cypress/e2e/sdk/tool_04.cy.js
@@ -0,0 +1,20 @@
+import { setupTest, goToConfigTab, checkTooltipAppears, createRequest } from "../../lib/sdk/setup";
+
+describe('4.Mixed tooltip (old Android SDK version)', () => {
+ it('4.1-Setup', function() {
+ setupTest();
+ cy.request('GET', createRequest('java-native-android', '23.12.1'))
+ .then((response) => {
+ // eslint-disable-next-line no-undef
+ expect(response.status).to.eq(200);
+ });
+ });
+ it('4.2-Reset', function() {
+ goToConfigTab();
+ });
+ it('4.3-Test', function() {
+ goToConfigTab(true);
+ checkTooltipAppears('success', 2, true);
+ checkTooltipAppears('warning', 19, true);
+ });
+});
\ No newline at end of file
diff --git a/ui-tests/cypress/e2e/sdk/tool_05.cy.js b/ui-tests/cypress/e2e/sdk/tool_05.cy.js
new file mode 100644
index 00000000000..f8ced86251f
--- /dev/null
+++ b/ui-tests/cypress/e2e/sdk/tool_05.cy.js
@@ -0,0 +1,20 @@
+import { setupTest, goToConfigTab, checkTooltipAppears, createRequest } from "../../lib/sdk/setup";
+
+describe('5.Mixed tooltip (old iOS SDK version)', () => {
+ it('5.1-Setup', function() {
+ setupTest();
+ cy.request('GET', createRequest('objc-native-ios', '24.12.1'))
+ .then((response) => {
+ // eslint-disable-next-line no-undef
+ expect(response.status).to.eq(200);
+ });
+ });
+ it('5.2-Reset', function() {
+ goToConfigTab();
+ });
+ it('5.3-Test', function() {
+ goToConfigTab(true);
+ checkTooltipAppears('success', 2, true);
+ checkTooltipAppears('warning', 19, true);
+ });
+});
\ No newline at end of file
diff --git a/ui-tests/cypress/e2e/sdk/tool_06.cy.js b/ui-tests/cypress/e2e/sdk/tool_06.cy.js
new file mode 100644
index 00000000000..0c159e8e180
--- /dev/null
+++ b/ui-tests/cypress/e2e/sdk/tool_06.cy.js
@@ -0,0 +1,19 @@
+import { setupTest, goToConfigTab, checkTooltipAppears, createRequest } from "../../lib/sdk/setup";
+
+describe('6.Danger tooltip (unsupported SDK)', () => {
+ it('6.1-Setup', function() {
+ setupTest();
+ cy.request('GET', createRequest('unity', '25.12.1'))
+ .then((response) => {
+ // eslint-disable-next-line no-undef
+ expect(response.status).to.eq(200);
+ });
+ });
+ it('6.2-Reset', function() {
+ goToConfigTab();
+ });
+ it('6.3-Test', function() {
+ goToConfigTab(true);
+ checkTooltipAppears('danger');
+ });
+});
\ No newline at end of file
diff --git a/ui-tests/cypress/e2e/sdk/tool_07.cy.js b/ui-tests/cypress/e2e/sdk/tool_07.cy.js
new file mode 100644
index 00000000000..885c839787b
--- /dev/null
+++ b/ui-tests/cypress/e2e/sdk/tool_07.cy.js
@@ -0,0 +1,19 @@
+import { setupTest, goToConfigTab, checkTooltipAppears, createRequest } from "../../lib/sdk/setup";
+
+describe('7.Success tooltip (latest Android SDK version)', () => {
+ it('7.1-Setup', function() {
+ setupTest();
+ cy.request('GET', createRequest('java-native-android', '25.12.1'))
+ .then((response) => {
+ // eslint-disable-next-line no-undef
+ expect(response.status).to.eq(200);
+ });
+ });
+ it('7.2-Reset', function() {
+ goToConfigTab();
+ });
+ it('7.3-Test', function() {
+ goToConfigTab(true);
+ checkTooltipAppears('success');
+ });
+});
\ No newline at end of file
diff --git a/ui-tests/cypress/e2e/sdk/tool_08.cy.js b/ui-tests/cypress/e2e/sdk/tool_08.cy.js
new file mode 100644
index 00000000000..6ccecd7c345
--- /dev/null
+++ b/ui-tests/cypress/e2e/sdk/tool_08.cy.js
@@ -0,0 +1,19 @@
+import { setupTest, goToConfigTab, checkTooltipAppears, createRequest } from "../../lib/sdk/setup";
+
+describe('8.Success tooltip (latest iOS SDK version)', () => {
+ it('8.1-Setup', function() {
+ setupTest();
+ cy.request('GET', createRequest('objc-native-ios', '25.12.1'))
+ .then((response) => {
+ // eslint-disable-next-line no-undef
+ expect(response.status).to.eq(200);
+ });
+ });
+ it('8.2-Reset', function() {
+ goToConfigTab();
+ });
+ it('8.3-Test', function() {
+ goToConfigTab(true);
+ checkTooltipAppears('success');
+ });
+});
\ No newline at end of file
diff --git a/ui-tests/cypress/e2e/sdk/tool_09.cy.js b/ui-tests/cypress/e2e/sdk/tool_09.cy.js
new file mode 100644
index 00000000000..1baec01dd10
--- /dev/null
+++ b/ui-tests/cypress/e2e/sdk/tool_09.cy.js
@@ -0,0 +1,23 @@
+import { setupTest, goToConfigTab, checkTooltipAppears, createRequest } from "../../lib/sdk/setup";
+
+describe('9.Mixed tooltip (multiple SDK versions)', () => {
+ it('9.1-Setup', function() {
+ setupTest();
+ cy.request('GET', createRequest('a', '26.12.1'));
+ cy.request('GET', createRequest('javascript_native_web', '25.12.1v2'));
+ cy.request('GET', createRequest('java-native-android', '22.12.1'));
+ cy.request('GET', createRequest('objc-native-ios-rc2', '24.12.1'))
+ .then((response) => {
+ // eslint-disable-next-line no-undef
+ expect(response.status).to.eq(200);
+ });
+ });
+ it('9.2-Reset', function() {
+ goToConfigTab();
+ });
+ it('9.3-Test', function() {
+ goToConfigTab(true);
+ checkTooltipAppears('success', 2, true);
+ checkTooltipAppears('warning', 19, true);
+ });
+});
diff --git a/ui-tests/cypress/lib/dashboard/manage/sdk/configurations.js b/ui-tests/cypress/lib/dashboard/manage/sdk/configurations.js
index 26fbe62f294..88f3cb00132 100644
--- a/ui-tests/cypress/lib/dashboard/manage/sdk/configurations.js
+++ b/ui-tests/cypress/lib/dashboard/manage/sdk/configurations.js
@@ -23,7 +23,7 @@ const verifyStaticElementsOfPage = () => {
cy.verifyElement({
labelElement: sdkConfiguratonsPageElements.PAGE_TITLE,
- labelText: "SDK Configuration (Experimental)",
+ labelText: "SDK Configuration",
tooltipElement: sdkConfiguratonsPageElements.PAGE_TITLE_TOOLTIP,
tooltipText: "This is experimental feature and not all SDKs and SDK versions yet support it. Refer to the SDK documentation for more information"
});
diff --git a/ui-tests/cypress/lib/sdk/setup.js b/ui-tests/cypress/lib/sdk/setup.js
new file mode 100644
index 00000000000..9e35bb221c3
--- /dev/null
+++ b/ui-tests/cypress/lib/sdk/setup.js
@@ -0,0 +1,88 @@
+/* eslint-disable no-undef */
+const navigationHelpers = require('../../support/navigations');
+const setupHelpers = require('../../lib/onboarding/setup');
+const initialSetupHelpers = require('../../lib/onboarding/initialSetup');
+const initialConsentHelpers = require('../../lib/onboarding/initialConsent');
+const quickstartPopoeverHelpers = require('../../support/components/quickstartPopover');
+
+const wait_L = 4000;
+const user = {
+ username: 'test',
+ email: 'a@a.com',
+ password: '111111111aA/'
+};
+
+const setupTest = () => {
+ cy.dropMongoDatabase(); // true in local tests
+ navigationHelpers.goToLoginPage();
+ setupHelpers.completeOnboardingSetup({
+ fullName: user.username,
+ emailAddress: user.email,
+ password: user.password,
+ confirmPassword: user.password,
+ isDemoApp: false
+ });
+ initialSetupHelpers.completeOnboardingInitialSetup({
+ isDemoApp: false,
+ appType: 'Mobile',
+ appName: 'My Mobile App',
+ appKey: '1',
+ timezone: 'Seoul'
+ });
+ initialConsentHelpers.completeOnboardingInitialConsent({
+ isEnableTacking: false,
+ isSubscribeToNewsletter: false
+ });
+ navigationHelpers.isNavigatedToDashboard();
+};
+
+const goToConfigTab = (nopop) => {
+ cy.visit('/login');
+ cy.get(':nth-child(3) > input').type(user.email);
+ cy.get(':nth-child(4) > input').type(user.password);
+ cy.get('#login-button').click();
+ cy.wait(wait_L);
+ cy.url().then(url => {
+ if (url.includes('not-responded-consent')) {
+ cy.get('[data-test-id="dont-enable-tracking-el-radio-wrapper"] > .el-radio__input > .el-radio__inner').click();
+ cy.get('.el-button > span').click();
+ }
+ });
+ cy.wait(wait_L);
+ cy.reload(true);
+ cy.wait(wait_L);
+ if (!nopop) {
+ quickstartPopoeverHelpers.closeQuickStartPopover();
+ }
+ navigationHelpers.goToSdkManagerPage();
+ cy.get('.white-bg > :nth-child(4)').click({force: true});
+ cy.wait(2000);
+ cy.get('.white-bg > :nth-child(1) > a > span').click({force: true});
+ cy.wait(2000);
+ cy.get('.white-bg > :nth-child(4)').click({force: true});
+ cy.wait(2000);
+};
+
+const createRequest = (sdkName, sdkVersion) => {
+ return 'http://localhost:3001/i?app_key=1&device_id=1&begin_session=1&sdk_name=' + sdkName + '&sdk_version=' + sdkVersion;
+};
+
+const checkTooltipAppears = (tooltip, count, early) => {
+ cy.get('.cly-vue-tooltip-icon.ion.ion-help-circled.tooltip-' + tooltip).should('have.length', count ? count : 22);
+
+ if (early) {
+ return;
+ }
+
+ cy.get('.cly-vue-tooltip-icon.ion.ion-help-circled.tooltip-neutral').should(tooltip == 'neutral' ? 'be.visible' : 'not.exist');
+ cy.get('.cly-vue-tooltip-icon.ion.ion-help-circled.tooltip-warning').should(tooltip == 'warning' ? 'be.visible' : 'not.exist');
+ cy.get('.cly-vue-tooltip-icon.ion.ion-help-circled.tooltip-danger').should(tooltip == 'danger' ? 'be.visible' : 'not.exist');
+ cy.get('.cly-vue-tooltip-icon.ion.ion-help-circled.tooltip-success').should(tooltip == 'success' ? 'be.visible' : 'not.exist');
+};
+
+module.exports = {
+ setupTest,
+ goToConfigTab,
+ checkTooltipAppears,
+ createRequest
+};
\ No newline at end of file
diff --git a/ui-tests/package.json b/ui-tests/package.json
index 68e9f9764b7..b8ffd38d103 100644
--- a/ui-tests/package.json
+++ b/ui-tests/package.json
@@ -5,7 +5,8 @@
"author": "Countly, Inc.",
"scripts": {
"cy:run:dashboard": "node ./cypress/fixtures/generators/generateFixtures.js && cypress run --record --key 00000000-0000-0000-0000-000000000000 --spec **/dashboard/**/*.cy.js --headless --no-runner-ui --browser chrome",
- "cy:run:onboarding": "cypress run --record --key 00000000-0000-0000-0000-000000000000 --spec **/onboarding/**/*.cy.js --headless --no-runner-ui --browser chrome"
+ "cy:run:onboarding": "cypress run --record --key 00000000-0000-0000-0000-000000000000 --spec **/onboarding/**/*.cy.js --headless --no-runner-ui --browser chrome",
+ "cy:run:sdk": "cypress run --record --key 00000000-0000-0000-0000-000000000000 --spec **/sdk/**/*.cy.js --headless --no-runner-ui --browser chrome"
},
"license": "ISC",
"dependencies": {