diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index d283622..b9bab18 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -13,10 +13,10 @@ name: "CodeQL" on: push: - branches: [ master ] + branches: [ master, staging ] pull_request: # The branches below must be a subset of the branches above - branches: [ master ] + branches: [ master, staging ] schedule: - cron: '30 5 * * 4' @@ -39,11 +39,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -54,7 +54,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v3 # ℹī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -68,4 +68,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/release_notice.yml b/.github/workflows/release_notice.yml new file mode 100644 index 0000000..e17315a --- /dev/null +++ b/.github/workflows/release_notice.yml @@ -0,0 +1,39 @@ +name: Release Notice +on: + release: + types: [published] + workflow_dispatch: +jobs: + build: + runs-on: ubuntu-latest + steps: + # To check the github context + - name: Dump Github context + env: + GITHUB_CONTEXT: ${{ toJSON(github) }} + run: echo "$GITHUB_CONTEXT" + - name: Send custom JSON data to Slack workflow + id: slack + uses: slackapi/slack-github-action@v1.23.0 + with: + # This data can be any valid JSON from a previous step in the GitHub Action + payload: | + { + "repository": "${{ github.repository }}", + "tag_name": "${{ github.event.release.tag_name }}", + "actor": "${{ github.actor }}", + "body": ${{ toJSON(github.event.release.body) }}, + "html_url": "${{ github.event.release.html_url }}" + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_RELEASE }} + - name: Send custom JSON data to Discord + uses: sarisia/actions-status-discord@v1.13.0 + with: + webhook: ${{ secrets.DISCORD_WEBHOOK_URL }} + nodetail: true + title: New ${{ github.repository }} version ${{ github.event.release.tag_name }} published by ${{ github.actor }} + description: | + Release URL: ${{ github.event.release.html_url }} + Click [here](https://github.com/Countly/countly-server/blob/master/CHANGELOG.md) to view the change log. + `${{ github.event.release.body }}` diff --git a/CHANGELOG.md b/CHANGELOG.md index d87f4cf..9f3de83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 24.10.0 +- Default max segmentation value count changed from 30 to 100 +- Mitigated an issue where SDK could create an unintended dump file +- Added a new init time config option (conf.storage_type) which can make user set the SDK storage option: + - File Storage + - Memory Only Storage +- Added a new init time config option (conf.custom_storage_method) which enables user to provide custom storage methods + ## 22.06.0 - Fixed a bug where remote config requests were rejected - Fixed a bug where empty storage object did cause some issues diff --git a/README.md b/README.md index a2d560d..32478c1 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,18 @@ -[![Codacy Badge](https://app.codacy.com/project/badge/Grade/ed5f07ef6f8b4503ac01b2dab190de01)](https://www.codacy.com/gh/Countly/countly-sdk-nodejs/dashboard?utm_source=github.com&utm_medium=referral&utm_content=Countly/countly-sdk-nodejs&utm_campaign=Badge_Grade) +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/ed5f07ef6f8b4503ac01b2dab190de01)](https://app.codacy.com/gh/Countly/countly-sdk-nodejs/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [![npm version](https://badge.fury.io/js/countly-sdk-nodejs.svg)](https://badge.fury.io/js/countly-sdk-nodejs) [![Inline docs](https://inch-ci.org/github/Countly/countly-sdk-nodejs.svg?branch=master)](https://inch-ci.org/github/Countly/countly-sdk-nodejs) # Countly NodeJS SDK -This repository contains the Countly NodeJS SDK, which can be integrated into NodeJS running device or server. The Countly NodeJS SDK is intended to be used with [Countly Community Edition](https://github.com/Countly/countly-server) or [Countly Enterprise Edition](https://count.ly/product). +This repository contains the Countly NodeJS SDK, which can be integrated into NodeJS running device or server. The Countly NodeJS SDK is intended to be used with [Countly Lite](https://countly.com/lite), [Countly Flex](https://countly.com/flex) or [Countly Enterprise](https://countly.com/enterprise). ## What is Countly? -[Countly](https://count.ly) is a product analytics solution and innovation enabler that helps teams track product performance and customer journey and behavior across [mobile](https://count.ly/mobile-analytics), [web](http://count.ly/web-analytics), +[Countly](https://count.ly) is a product analytics solution and innovation enabler that helps teams track product performance and customer journey and behavior across [mobile](https://count.ly/mobile-analytics), [web](https://count.ly/web-analytics), and [desktop](https://count.ly/desktop-analytics) applications. [Ensuring privacy by design](https://count.ly/privacy-by-design), Countly allows you to innovate and enhance your products to provide personalized and customized customer experiences, and meet key business and revenue goals. Track, measure, and take action - all without leaving Countly. * **Questions or feature requests?** [Join the Countly Community on Discord](https://discord.gg/countly) -* **Looking for the Countly Server?** [Countly Community Edition repository](https://github.com/Countly/countly-server) +* **Looking for the Countly Server?** [Countly Server repository](https://github.com/Countly/countly-server) * **Looking for other Countly SDKs?** [An overview of all Countly SDKs for mobile, web and desktop](https://support.count.ly/hc/en-us/articles/360037236571-Downloading-and-Installing-SDKs#officially-supported-sdks) ## Integrating Countly SDK in your projects diff --git a/examples/apm_example.js b/examples/apm_example.js index be2da01..93d9163 100644 --- a/examples/apm_example.js +++ b/examples/apm_example.js @@ -1,10 +1,17 @@ //since we need to test crashing the app /*global app*/ +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") +} + var Countly = require("../lib/countly.js"); Countly.init({ - app_key: "YOUR_APP_KEY", - url: "https://try.count.ly", //your server goes here + app_key: COUNTLY_APP_KEY, + url: COUNTLY_SERVER_KEY, //your server goes here debug: true }); diff --git a/examples/bulk_import_example.js b/examples/bulk_import_example.js index 83da019..65a9d76 100644 --- a/examples/bulk_import_example.js +++ b/examples/bulk_import_example.js @@ -1,8 +1,15 @@ +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") +} + var CountlyBulk = require("../lib/countly").Bulk; var server = new CountlyBulk({ - app_key: "YOUR_APP_KEY", - url: "https://try.count.ly", //your server goes here + app_key: COUNTLY_APP_KEY, + url: COUNTLY_SERVER_KEY, //your server goes here debug: true }); diff --git a/examples/example.js b/examples/example.js index 65fdd35..635f7e7 100644 --- a/examples/example.js +++ b/examples/example.js @@ -1,14 +1,20 @@ //since we need to test crashing the app /*global runthis, crashDaApp*/ +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") +} + var Countly = require("../lib/countly.js"); Countly.init({ - app_key: "YOUR_APP_KEY", - url: "https://try.count.ly", //your server goes here + app_key: COUNTLY_APP_KEY, + url: COUNTLY_SERVER_KEY, //your server goes here debug: true }); - Countly.begin_session(); Countly.track_errors(); diff --git a/examples/multi-process.js b/examples/multi-process.js index 5e1e711..59086fe 100644 --- a/examples/multi-process.js +++ b/examples/multi-process.js @@ -13,11 +13,18 @@ else if (cluster.isWorker) { console.log("I am worker " + cluster.worker.id); } +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") +} + var Countly = require("../lib/countly.js"); Countly.init({ - app_key: "YOUR_APP_KEY", - url: "https://try.count.ly", //your server goes here + app_key: COUNTLY_APP_KEY, + url: COUNTLY_SERVER_KEY, //your server goes here debug: true }); diff --git a/lib/countly-bulk-user.js b/lib/countly-bulk-user.js index e32aace..0f45a42 100644 --- a/lib/countly-bulk-user.js +++ b/lib/countly-bulk-user.js @@ -49,7 +49,7 @@ function CountlyBulkUser(conf) { var sessionStart = 0; var maxKeyLength = 128; var maxValueSize = 256; - var maxSegmentationValues = 30; + var maxSegmentationValues = 100; var maxBreadcrumbCount = 100; var maxStackTraceLinesPerThread = 30; var maxStackTraceLineLength = 200; diff --git a/lib/countly-bulk.js b/lib/countly-bulk.js index 52e78e3..a7ecc0e 100644 --- a/lib/countly-bulk.js +++ b/lib/countly-bulk.js @@ -18,13 +18,14 @@ * server.add_request({begin_session:1, metrics:{_os:"Linux"}, device_id:"users_device_id", events:[{key:"Test", count:1}]}); */ -var fs = require("fs"); -var path = require("path"); var http = require("http"); var https = require("https"); var cluster = require("cluster"); var cc = require("./countly-common"); var BulkUser = require("./countly-bulk-user"); +var CountlyStorage = require("./countly-storage"); + +CountlyBulk.StorageTypes = cc.storageTypeEnums; /** * @lends module:lib/countly-bulk @@ -38,9 +39,8 @@ var BulkUser = require("./countly-bulk-user"); * @param {number} [conf.fail_timeout=60] - set time in seconds to wait after failed connection to server in seconds * @param {number} [conf.session_update=60] - how often in seconds should session be extended * @param {number} [conf.max_events=100] - maximum amount of events to send in one batch - * @param {boolean} [conf.persist_queue=false] - persistently store queue until processed, default is false if you want to keep queue in memory and process all in one process run * @param {boolean} [conf.force_post=false] - force using post method for all requests - * @param {string} [conf.storage_path="../bulk_data/"] - where SDK would store data, including id, queues, etc + * @param {string} [conf.storage_path] - where SDK would store data, including id, queues, etc * @param {string} [conf.http_options=] - function to get http options by reference and overwrite them, before running each request * @param {number} [conf.max_key_length=128] - maximum size of all string keys * @param {number} [conf.max_value_size=256] - maximum size of all values in our key-value pairs (Except "picture" field, that has a limit of 4096 chars) @@ -48,6 +48,9 @@ var BulkUser = require("./countly-bulk-user"); * @param {number} [conf.max_breadcrumb_count=100] - maximum amount of breadcrumbs that can be recorded before the oldest one is deleted * @param {number} [conf.max_stack_trace_lines_per_thread=30] - maximum amount of stack trace lines would be recorded per thread * @param {number} [conf.max_stack_trace_line_length=200] - maximum amount of characters are allowed per stack trace line. This limits also the crash message length + * @param {StorageTypes} conf.storage_type - to determine which storage type is going to be applied + * @param {Object} conf.custom_storage_method - user given storage methods + * @param {boolean} [conf.persist_queue=false] - *DEPRECATED* persistent mode instead of using in-memory queue. Use storage_type and storage_path instead * @example * var server = new CountlyBulk({ * app_key: "{YOUR-API-KEY}", @@ -56,7 +59,7 @@ var BulkUser = require("./countly-bulk-user"); * }); */ function CountlyBulk(conf) { - var SDK_VERSION = "22.06.0"; + var SDK_VERSION = "24.10.0"; var SDK_NAME = "javascript_native_nodejs_bulk"; var empty_queue_callback = null; @@ -69,12 +72,10 @@ function CountlyBulk(conf) { var readyToProcess = true; var maxKeyLength = 128; var maxValueSize = 256; - var maxSegmentationValues = 30; + var maxSegmentationValues = 100; var maxBreadcrumbCount = 100; var maxStackTraceLinesPerThread = 30; var maxStackTraceLineLength = 200; - var __data = {}; - cc.debugBulk = conf.debug || false; if (!conf.app_key) { cc.log(cc.logLevelEnums.ERROR, "CountlyBulk, 'app_key' is missing."); @@ -96,7 +97,6 @@ function CountlyBulk(conf) { conf.max_events = conf.max_events || 100; conf.force_post = conf.force_post || false; conf.persist_queue = conf.persist_queue || false; - conf.storage_path = conf.storage_path || "../bulk_data/"; conf.http_options = conf.http_options || null; conf.maxKeyLength = conf.max_key_length || maxKeyLength; conf.maxValueSize = conf.max_value_size || maxValueSize; @@ -105,20 +105,13 @@ function CountlyBulk(conf) { conf.maxStackTraceLinesPerThread = conf.max_stack_trace_lines_per_thread || maxStackTraceLinesPerThread; conf.maxStackTraceLineLength = conf.max_stack_trace_line_length || maxStackTraceLineLength; - var mainDir = path.resolve(__dirname, conf.storage_path); - if (conf.persist_queue) { - try { - if (!fs.existsSync(mainDir)) { - fs.mkdirSync(mainDir); - } - } - catch (ex) { - // problem creating directory - // eslint-disable-next-line no-console - cc.log(cc.logLevelEnums.ERROR, "CountlyBulk, Failed white creating the '/data' directory. Error: ", ex.stack); - } + // bulk mode is memory only by default + if (typeof conf.storage_type === "undefined" && conf.persist_queue === false) { + conf.storage_type = CountlyBulk.StorageTypes.MEMORY; } + CountlyStorage.initStorage(conf.storage_path, conf.storage_type, true, conf.custom_storage_method); + this.conf = conf; /** * Add raw request with provided query string parameters @@ -157,7 +150,7 @@ function CountlyBulk(conf) { requestQueue.push(query); cc.log(cc.logLevelEnums.INFO, "CountlyBulk add_request, Adding request to the queue."); - storeSet("cly_req_queue", requestQueue); + CountlyStorage.storeSet("cly_req_queue", requestQueue); } else { cc.log(cc.logLevelEnums.INFO, "CountlyBulk add_request, Sending message to the parent process. Adding the raw request to the queue."); @@ -205,7 +198,7 @@ function CountlyBulk(conf) { cc.log(cc.logLevelEnums.INFO, "CountlyBulk add_bulk_request, adding the request into queue."); requestQueue.push(query); } - storeSet("cly_req_queue", requestQueue); + CountlyStorage.storeSet("cly_req_queue", requestQueue); } else { cc.log(cc.logLevelEnums.INFO, "CountlyBulk add_bulk_request, Sending message to the parent process. Adding the raw request to the queue."); @@ -260,7 +253,7 @@ function CountlyBulk(conf) { eventQueue[device_id] = []; } eventQueue[device_id].push(e); - storeSet("cly_bulk_event", eventQueue); + CountlyStorage.storeSet("cly_bulk_event", eventQueue); } else { cc.log(cc.logLevelEnums.INFO, `CountlyBulk add_event, Sending message to the parent process. Adding event: [${event.key}].`); @@ -358,7 +351,7 @@ function CountlyBulk(conf) { */ function toBulkRequestQueue(bulkRequest) { bulkQueue.push(bulkRequest); - storeSet("cly_bulk_queue", bulkQueue); + CountlyStorage.storeSet("cly_bulk_queue", bulkQueue); } var self = this; @@ -384,7 +377,7 @@ function CountlyBulk(conf) { } if (eventChanges) { isEmpty = false; - storeSet("cly_bulk_event", eventQueue); + CountlyStorage.storeSet("cly_bulk_event", eventQueue); } // process request queue into bulk requests @@ -398,7 +391,7 @@ function CountlyBulk(conf) { var requests = requestQueue.splice(0, conf.bulk_size); toBulkRequestQueue({ app_key: conf.app_key, requests: JSON.stringify(requests) }); } - storeSet("cly_req_queue", requestQueue); + CountlyStorage.storeSet("cly_req_queue", requestQueue); } // process bulk request queue @@ -413,7 +406,7 @@ function CountlyBulk(conf) { bulkQueue.unshift(res); failTimeout = cc.getTimestamp() + conf.fail_timeout; } - storeSet("cly_bulk_queue", bulkQueue); + CountlyStorage.storeSet("cly_bulk_queue", bulkQueue); readyToProcess = true; }, "heartBeat", false); } @@ -594,111 +587,6 @@ function CountlyBulk(conf) { } } - /** - * Read value from file - * @param {String} key - key for file - * @returns {varies} value in file - */ - var readFile = function(key) { - var data; - if (conf.persist_queue) { - var dir = path.resolve(__dirname, `${conf.storage_path}__${key}.json`); - - // try reading data file - try { - data = fs.readFileSync(dir); - } - catch (ex) { - // there was no file, probably new init - cc.log(cc.logLevelEnums.ERROR, "CountlyBulk readFile, Nothing to read. Might be first init. Error: ", ex); - data = null; - } - - try { - // trying to parse json string - data = JSON.parse(data); - } - catch (ex) { - // problem parsing, corrupted file? - cc.log(cc.logLevelEnums.ERROR, "CountlyBulk readFile, Problem while parsing. Error:", ex.stack); - // backup corrupted file data - fs.writeFile(path.resolve(__dirname, `${conf.storage_path}__${key}.${cc.getTimestamp()}${Math.random()}.json`), data, () => {}); - // start with new clean object - data = null; - } - } - return data; - }; - - var asyncWriteLock = false; - var asyncWriteQueue = []; - - /** - * Write to file and process queue while in asyncWriteLock - * @param {String} key - key for value to store - * @param {varies} value - value to store - * @param {Function} callback - callback to call when done storing - */ - var writeFile = function(key, value, callback) { - var ob = {}; - ob[key] = value; - var dir = path.resolve(__dirname, `${conf.storage_path}__${key}.json`); - fs.writeFile(dir, JSON.stringify(ob), (err) => { - if (err) { - // eslint-disable-next-line no-console - cc.log(cc.logLevelEnums.ERROR, "CountlyBulk writeFile, Problem while writing. Error:", err); - } - if (typeof callback === "function") { - callback(err); - } - if (asyncWriteQueue.length) { - setTimeout(() => { - var arr = asyncWriteQueue.shift(); - writeFile(arr[0], arr[1], arr[2]); - }, 0); - } - else { - asyncWriteLock = false; - } - }); - }; - - /** - * Save value in storage - * @param {String} key - key for value to store - * @param {varies} value - value to store - * @param {Function} callback - callback to call when done storing - */ - var storeSet = function(key, value, callback) { - __data[key] = value; - if (!asyncWriteLock) { - asyncWriteLock = true; - writeFile(key, value, callback); - } - else { - asyncWriteQueue.push([key, value, callback]); - } - }; - - /** - * Get value from storage - * @param {String} key - key of value to get - * @param {varies} def - default value to use if not set - * @returns {varies} value for the key - */ - var storeGet = function(key, def) { - if (typeof __data[key] === "undefined") { - var ob = readFile(key); - if (!ob) { - __data[key] = def; - } - else { - __data[key] = ob[key]; - } - } - return __data[key]; - }; - // listen to current workers if (cluster.workers) { for (var id in cluster.workers) { @@ -711,9 +599,30 @@ function CountlyBulk(conf) { worker.on("message", handleWorkerMessage); }); - var requestQueue = storeGet("cly_req_queue", []); - var eventQueue = storeGet("cly_bulk_event", {}); - var bulkQueue = storeGet("cly_bulk_queue", []); + var requestQueue = CountlyStorage.storeGet("cly_req_queue", []); + var eventQueue = CountlyStorage.storeGet("cly_bulk_event", {}); + var bulkQueue = CountlyStorage.storeGet("cly_bulk_queue", []); + /** + * getBulkEventQueue is a testing purposed method which returns the event queue object + * @returns {Object} eventQueue + */ + this._getBulkEventQueue = function() { + return eventQueue; + }; + /** + * getBulkRequestQueue is a testing purposed method which returns the request queue object + * @returns {Object} requestQueue + */ + this._getBulkRequestQueue = function() { + return requestQueue; + }; + /** + * getBulkQueue is a testing purposed method which returns the bulk queue object + * @returns {Object} bulkQueue + */ + this._getBulkQueue = function() { + return bulkQueue; + }; } module.exports = CountlyBulk; diff --git a/lib/countly-common.js b/lib/countly-common.js index d829250..c721bb1 100644 --- a/lib/countly-common.js +++ b/lib/countly-common.js @@ -51,6 +51,11 @@ var cc = { ACTION: '[CLY]_action', }, + storageTypeEnums: { + FILE: "file", + MEMORY: "memory", + }, + /** * Get current timestamp * @returns {number} unix timestamp in seconds diff --git a/lib/countly-storage.js b/lib/countly-storage.js new file mode 100644 index 0000000..3aeb309 --- /dev/null +++ b/lib/countly-storage.js @@ -0,0 +1,348 @@ +const fs = require('fs'); +const path = require('path'); +var cc = require("./countly-common"); + +// Constants +const defaultPath = "../data/"; // Default storage path +const defaultBulkPath = "../bulk_data/"; // Default bulk storage path +const StorageTypes = cc.storageTypeEnums; +const defaultStorageType = StorageTypes.FILE; +const customTypeName = "custom"; + +var storagePath; +let storageMethod = {}; +var __cache = {}; +var asyncWriteLock = false; +var asyncWriteQueue = []; + +/** + * Sets the storage method, by default sets file storage and storage path. + * @param {String} userPath - User provided storage path + * @param {StorageTypes} storageType - Whether to use memory only storage or not + * @param {Boolean} isBulk - Whether the storage is for bulk data + * @param {varies} customStorageMethod - Storage methods provided by the user + */ +var initStorage = function(userPath, storageType, isBulk = false, customStorageMethod = null) { + cc.log(cc.logLevelEnums.INFO, `Initializing storage with userPath: [${userPath}], storageType: [${storageType}], isBulk: [${isBulk}], customStorageMethod type: [${typeof customStorageMethod}].`); + + // set storage type + storageType = storageType || defaultStorageType; + storageMethod = fileStorage; // file storage is default + + if (storageType === StorageTypes.MEMORY) { + storageMethod = memoryStorage; + cc.log(cc.logLevelEnums.DEBUG, `Using memory storage!`); + } + + // at this point we either use memory or file storage. If custom storage is provided, check if it is valid and use it instead + if (isCustomStorageValid(customStorageMethod)) { + storageMethod = customStorageMethod; + storageType = customTypeName; + cc.log(cc.logLevelEnums.DEBUG, `Using custom storage!`); + } + + // set storage path if not memory storage + if (storageType !== StorageTypes.MEMORY) { + setStoragePath(userPath, isBulk); + } +}; + +// Memory-only storage methods +const memoryStorage = { + /** + * Save value in memory + * @param {String} key - key for value to store + * @param {varies} value - value to store + * @param {Function} callback - callback to call when done storing + */ + storeSet: function(key, value, callback) { + if (key) { + cc.log(cc.logLevelEnums.DEBUG, `storeSet, Setting key: [${key}] & value: [${value}]!`); + __cache[key] = value; + if (typeof callback === "function") { + callback(null); + } + } + else { + cc.log(cc.logLevelEnums.WARNING, `storeSet, Provioded key: [${key}] is null!`); + } + }, + /** + * Get value from memory + * @param {String} key - key of value to get + * @param {varies} def - default value to use if not set + * @returns {varies} value for the key + */ + storeGet: function(key, def) { + cc.log(cc.logLevelEnums.DEBUG, `storeGet, Fetching item from memory with key: [${key}].`); + return typeof __cache[key] !== "undefined" ? __cache[key] : def; + }, + /** + * Remove value from memory + * @param {String} key - key of value to remove + */ + storeRemove: function(key) { + delete __cache[key]; + }, +}; + +// File storage methods +const fileStorage = { + /** + * Save value in storage + * @param {String} key - key for value to store + * @param {varies} value - value to store + * @param {Function} callback - callback to call when done storing + */ + storeSet: function(key, value, callback) { + __cache[key] = value; + if (!asyncWriteLock) { + asyncWriteLock = true; + writeFile(key, value, callback); + } + else { + asyncWriteQueue.push([key, value, callback]); + } + }, + /** + * Get value from storage + * @param {String} key - key of value to get + * @param {varies} def - default value to use if not set + * @returns {varies} value for the key + */ + storeGet: function(key, def) { + cc.log(cc.logLevelEnums.DEBUG, `storeGet, Fetching item from storage with key: [${key}].`); + if (typeof __cache[key] === "undefined") { + var ob = readFile(key); + var obLen; + // check if the 'read object' is empty or not + try { + obLen = Object.keys(ob).length; + } + catch (error) { + // if we can not even asses length set it to 0 so we can return the default value + obLen = 0; + } + + // if empty or falsy set default value + if (!ob || obLen === 0) { + __cache[key] = def; + } + // else set the value read file has + else { + __cache[key] = ob[key]; + } + } + return __cache[key]; + }, + storeRemove: function(key) { + delete __cache[key]; + var filePath = path.resolve(__dirname, `${getStoragePath()}__${key}.json`); + fs.access(filePath, fs.constants.F_OK, (accessErr) => { + if (accessErr) { + cc.log(cc.logLevelEnums.WARNING, `storeRemove, No file found with key: [${key}]. Nothing to remove.`); + return; + } + fs.unlink(filePath, (err) => { + if (err) { + cc.log(cc.logLevelEnums.ERROR, `storeRemove, Failed to remove file with key: [${key}]. Error: [${err.message}].`); + } + else { + cc.log(cc.logLevelEnums.INFO, `storeRemove, Successfully removed file with key: [${key}].`); + } + }); + }); + }, +}; + +var isCustomStorageValid = function(storage) { + if (!storage) { + return false; + } + if (typeof storage.storeSet !== 'function') { + return false; + } + if (typeof storage.storeGet !== 'function') { + return false; + } + if (typeof storage.storeRemove !== 'function') { + return false; + } + return true; +}; + +/** + * Sets the storage path, defaulting to a specified path if none is provided. + * @param {String} userPath - User provided storage path + * @param {Boolean} isBulk - Whether the storage is for bulk data + */ +var setStoragePath = function(userPath, isBulk = false) { + storagePath = userPath || (isBulk ? defaultBulkPath : defaultPath); + + createDirectory(path.resolve(__dirname, storagePath)); +}; + +/** + * Returns the current storage path + * @returns {String} storagePath + */ +var getStoragePath = function() { + return storagePath; +}; + +/** + * Creates a directory if it doesn't exist + * @param {String} dir - The directory path + */ +var createDirectory = function(dir) { + try { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + } + catch (ex) { + cc.log(cc.logLevelEnums.ERROR, `Failed to create directory at ${dir}: ${ex.stack}`); + } +}; + +/** + * Resets storage-related variables to their initial state + */ +var resetStorage = function() { + storagePath = undefined; + __cache = {}; + asyncWriteLock = false; + asyncWriteQueue = []; + storageMethod = {}; +}; + +/** + * Read value from file + * @param {String} key - key for file + * @returns {varies} value in file + */ +var readFile = function(key) { + var dir = path.resolve(__dirname, `${getStoragePath()}__${key}.json`); + // try reading data file + var data; + try { + data = fs.readFileSync(dir, 'utf8'); // read file as string + } + catch (ex) { + // there was no file, probably new init + cc.log(cc.logLevelEnums.WARN, `readFile, File not found for key: [${key}]. Returning null.`); + return null; // early exit if file doesn't exist + } + + // early exit if file is empty or whitespace + if (!data.trim()) { + cc.log(cc.logLevelEnums.WARN, `readFile, File is empty or contains only whitespace for key: [${key}]. Returning null.`); + return null; + } + + try { + // trying to parse json string + data = JSON.parse(data); + } + catch (ex) { + // problem parsing, corrupted file? + cc.log(cc.logLevelEnums.ERROR, `readFile, Failed to parse the file with key: [${key}]. Error: [${ex}].`); + // backup corrupted file data + fs.writeFile(path.resolve(__dirname, `${getStoragePath()}__${key}.${cc.getTimestamp()}${Math.random()}.json`), data, () => { }); + // start with new clean object + return null; // return null in case of corrupted data + } + return data; +}; + +/** + * Force store data synchronously on unrecoverable errors to preserve it for next launch + */ +var forceStore = function() { + for (var i in __cache) { + var dir = path.resolve(__dirname, `${getStoragePath()}__${i}.json`); + var ob = {}; + ob[i] = __cache[i]; + try { + fs.writeFileSync(dir, JSON.stringify(ob)); + } + catch (ex) { + // tried to save whats possible + cc.log(cc.logLevelEnums.ERROR, `forceStore, Saving files failed. Error: [${ex}].`); + } + } +}; + +/** + * Write to file and process queue while in asyncWriteLock + * @param {String} key - key for value to store + * @param {varies} value - value to store + * @param {Function} callback - callback to call when done storing + */ +var writeFile = function(key, value, callback) { + var ob = {}; + ob[key] = value; + var dir = path.resolve(__dirname, `${getStoragePath()}__${key}.json`); + fs.writeFile(dir, JSON.stringify(ob), (err) => { + if (err) { + cc.log(cc.logLevelEnums.ERROR, `writeFile, Writing files failed. Error: [${err}].`); + } + if (typeof callback === "function") { + callback(err); + } + if (asyncWriteQueue.length) { + setTimeout(() => { + var arr = asyncWriteQueue.shift(); + cc.log(cc.logLevelEnums.DEBUG, "writeFile, Dequeued array:", arr); + if (arr) { + writeFile(arr[0], arr[1], arr[2]); + } + }, 0); + } + else { + asyncWriteLock = false; + } + }); +}; + +var storeSet = function(key, value, callback) { + storageMethod.storeSet(key, value, callback); +}; + +var storeGet = function(key, def) { + return storageMethod.storeGet(key, def); +}; + +var storeRemove = function(key) { + storageMethod.storeRemove(key); +}; + +/** + * Disclaimer: This method is mainly for testing purposes. + * @returns {StorageTypes} Returns the active storage type for the SDK + */ +var getStorageType = function() { + if (storageMethod === memoryStorage) { + return StorageTypes.MEMORY; + } + + if (storageMethod === fileStorage) { + return StorageTypes.FILE; + } + + return null; +}; + +module.exports = { + initStorage, + storeSet, + storeGet, + storeRemove, + writeFile, + forceStore, + getStoragePath, + setStoragePath, + resetStorage, + readFile, + getStorageType, +}; \ No newline at end of file diff --git a/lib/countly.js b/lib/countly.js index 9d39451..761ca7a 100644 --- a/lib/countly.js +++ b/lib/countly.js @@ -20,20 +20,19 @@ * Countly.begin_session(); */ -var fs = require("fs"); var os = require("os"); -var path = require("path"); var http = require("http"); var https = require("https"); var cluster = require("cluster"); var cc = require("./countly-common"); var Bulk = require("./countly-bulk"); +var CountlyStorage = require("./countly-storage"); var Countly = {}; - +Countly.StorageTypes = cc.storageTypeEnums; Countly.Bulk = Bulk; (function() { - var SDK_VERSION = "22.06.0"; + var SDK_VERSION = "24.10.0"; var SDK_NAME = "javascript_native_nodejs"; var inited = false; @@ -67,13 +66,12 @@ Countly.Bulk = Bulk; var startTime; var maxKeyLength = 128; var maxValueSize = 256; - var maxSegmentationValues = 30; + var maxSegmentationValues = 100; var maxBreadcrumbCount = 100; var maxStackTraceLinesPerThread = 30; var maxStackTraceLineLength = 200; - var __data = {}; var deviceIdType = null; - + var heartBeatTimer = null; /** * Array with list of available features that you can require consent for */ @@ -104,7 +102,7 @@ Countly.Bulk = Bulk; * @param {boolean} [conf.force_post=false] - force using post method for all requests * @param {boolean} [conf.clear_stored_device_id=false] - set it to true if you want to erase the stored device ID * @param {boolean} [conf.test_mode=false] - set it to true if you want to initiate test_mode - * @param {string} [conf.storage_path="../data/"] - where SDK would store data, including id, queues, etc + * @param {string} [conf.storage_path] - where SDK would store data, including id, queues, etc * @param {boolean} [conf.require_consent=false] - pass true if you are implementing GDPR compatible consent management. It would prevent running any functionality without proper consent * @param {boolean|function} [conf.remote_config=false] - Enable automatic remote config fetching, provide callback function to be notified when fetching done * @param {function} [conf.http_options=] - function to get http options by reference and overwrite them, before running each request @@ -124,6 +122,8 @@ Countly.Bulk = Bulk; * @param {string} conf.metrics._density - screen density of the device * @param {string} conf.metrics._locale - locale or language of the device in ISO format * @param {string} conf.metrics._store - source from where the user/device/installation came from + * @param {StorageTypes} conf.storage_type - to determine which storage type is going to be applied + * @param {Object} conf.custom_storage_method - user given storage methods * @example * Countly.init({ * app_key: "{YOUR-APP-KEY}", @@ -159,7 +159,6 @@ Countly.Bulk = Bulk; Countly.city = conf.city || Countly.city || null; Countly.ip_address = conf.ip_address || Countly.ip_address || null; Countly.force_post = conf.force_post || Countly.force_post || false; - Countly.storage_path = conf.storage_path || Countly.storage_path || "../data/"; Countly.require_consent = conf.require_consent || Countly.require_consent || false; Countly.remote_config = conf.remote_config || Countly.remote_config || false; Countly.http_options = conf.http_options || Countly.http_options || null; @@ -169,25 +168,18 @@ Countly.Bulk = Bulk; Countly.maxBreadcrumbCount = conf.max_breadcrumb_count || Countly.max_breadcrumb_count || conf.max_logs || Countly.max_logs || maxBreadcrumbCount; Countly.maxStackTraceLinesPerThread = conf.max_stack_trace_lines_per_thread || Countly.max_stack_trace_lines_per_thread || maxStackTraceLinesPerThread; Countly.maxStackTraceLineLength = conf.max_stack_trace_line_length || Countly.max_stack_trace_line_length || maxStackTraceLineLength; - + conf.storage_path = conf.storage_path || Countly.storage_path; + conf.storage_type = conf.storage_type || Countly.storage_type; // Common module debug value is set to init time debug value cc.debug = conf.debug; - var dir = path.resolve(__dirname, Countly.storage_path); - try { - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } - } - catch (ex) { - // problem creating data dir - cc.log(cc.logLevelEnums.ERROR, `init, Failed to create the '/data' folder: ${ex}`); - } + CountlyStorage.initStorage(conf.storage_path, conf.storage_type, false, conf.custom_storage_method); + // clear stored device ID if flag is set if (conf.clear_stored_device_id) { cc.log(cc.logLevelEnums.WARNING, "init, clear_stored_device_id is true, erasing the stored ID."); - storeSet("cly_id", null); - storeSet("cly_id_type", null); + CountlyStorage.storeSet("cly_id", null); + CountlyStorage.storeSet("cly_id_type", null); } if (Countly.url === "") { @@ -220,7 +212,7 @@ Countly.Bulk = Bulk; cc.log(cc.logLevelEnums.DEBUG, `init, IP address: [${Countly.ip_address}].`); } cc.log(cc.logLevelEnums.DEBUG, `init, Force POST requests: [${Countly.force_post}].`); - cc.log(cc.logLevelEnums.DEBUG, `init, Storage path: [${Countly.storage_path}].`); + cc.log(cc.logLevelEnums.DEBUG, `init, Storage path: [${CountlyStorage.getStoragePath()}].`); cc.log(cc.logLevelEnums.DEBUG, `init, Require consent: [${Countly.require_consent}].`); if (Countly.remote_config) { cc.log(cc.logLevelEnums.DEBUG, `init, Automatic Remote Configuration is on.`); @@ -233,8 +225,8 @@ Countly.Bulk = Bulk; if (cluster.isMaster) { // fetch stored ID and ID type - var storedId = storeGet("cly_id", null); - var storedIdType = storeGet("cly_id_type", null); + var storedId = CountlyStorage.storeGet("cly_id", null); + var storedIdType = CountlyStorage.storeGet("cly_id_type", null); // if there was a stored ID if (storedId !== null) { Countly.device_id = storedId; @@ -263,12 +255,12 @@ Countly.Bulk = Bulk; deviceIdType = cc.deviceIdTypeEnums.SDK_GENERATED; } // save the ID and ID type - storeSet("cly_id", Countly.device_id); - storeSet("cly_id_type", deviceIdType); + CountlyStorage.storeSet("cly_id", Countly.device_id); + CountlyStorage.storeSet("cly_id_type", deviceIdType); // create queues - requestQueue = storeGet("cly_queue", []); - eventQueue = storeGet("cly_event", []); - remoteConfigs = storeGet("cly_remote_configs", {}); + requestQueue = CountlyStorage.storeGet("cly_queue", []); + eventQueue = CountlyStorage.storeGet("cly_event", []); + remoteConfigs = CountlyStorage.storeGet("cly_remote_configs", {}); heartBeat(); // listen to current workers if (cluster.workers) { @@ -324,12 +316,15 @@ Countly.Bulk = Bulk; lastParams = {}; maxKeyLength = 128; maxValueSize = 256; - maxSegmentationValues = 30; + maxSegmentationValues = 100; maxBreadcrumbCount = 100; maxStackTraceLinesPerThread = 30; maxStackTraceLineLength = 200; - __data = {}; deviceIdType = null; + if (heartBeatTimer) { + clearInterval(heartBeatTimer); + heartBeatTimer = null; + } // cc DEBUG cc.debug = false; @@ -344,6 +339,7 @@ Countly.Bulk = Bulk; // device_id Countly.device_id = undefined; + Countly.device_id_type = undefined; Countly.remote_config = undefined; Countly.require_consent = false; Countly.debug = undefined; @@ -354,12 +350,9 @@ Countly.Bulk = Bulk; Countly.city = undefined; Countly.ip_address = undefined; Countly.force_post = undefined; - Countly.storage_path = undefined; Countly.require_consent = undefined; Countly.http_options = undefined; - - asyncWriteLock = false; - asyncWriteQueue = []; + CountlyStorage.resetStorage(); }; /** @@ -649,7 +642,7 @@ Countly.Bulk = Bulk; if (eventQueue.length > 0) { toRequestQueue({ events: JSON.stringify(eventQueue) }); eventQueue = []; - storeSet("cly_event", eventQueue); + CountlyStorage.storeSet("cly_event", eventQueue); } // end current session Countly.end_session(); @@ -661,8 +654,8 @@ Countly.Bulk = Bulk; var oldId = Countly.device_id; Countly.device_id = newId; deviceIdType = cc.deviceIdTypeEnums.DEVELOPER_SUPPLIED; - storeSet("cly_id", Countly.device_id); - storeSet("cly_id_type", deviceIdType); + CountlyStorage.storeSet("cly_id", Countly.device_id); + CountlyStorage.storeSet("cly_id_type", deviceIdType); if (merge) { if (Countly.check_any_consent()) { toRequestQueue({ old_device_id: oldId }); @@ -678,7 +671,7 @@ Countly.Bulk = Bulk; if (Countly.remote_config) { remoteConfigs = {}; if (cluster.isMaster) { - storeSet("cly_remote_configs", remoteConfigs); + CountlyStorage.storeSet("cly_remote_configs", remoteConfigs); } Countly.fetch_remote_config(Countly.remote_config); } @@ -733,6 +726,7 @@ Countly.Bulk = Bulk; add_cly_events(event); } }; + /** * Add events to event queue * @memberof Countly._internals @@ -757,7 +751,7 @@ Countly.Bulk = Bulk; e.dow = date.getDay(); cc.log(cc.logLevelEnums.DEBUG, "add_cly_events, Adding event: ", event); eventQueue.push(e); - storeSet("cly_event", eventQueue); + CountlyStorage.storeSet("cly_event", eventQueue); } else { process.send({ cly: { event } }); @@ -1072,7 +1066,7 @@ Countly.Bulk = Bulk; process.on("uncaughtException", (err) => { recordError(err, false); if (cluster.isMaster) { - forceStore(); + CountlyStorage.forceStore(); } // eslint-disable-next-line no-console console.error(`${(new Date()).toUTCString()} uncaughtException:`, err.message); @@ -1085,7 +1079,7 @@ Countly.Bulk = Bulk; var err = new Error(`Unhandled rejection (reason: ${reason && reason.stack ? reason.stack : reason}).`); recordError(err, false); if (cluster.isMaster) { - forceStore(); + CountlyStorage.forceStore(); } // eslint-disable-next-line no-console console.error(`${(new Date()).toUTCString()} unhandledRejection:`, err.message); @@ -1168,7 +1162,7 @@ Countly.Bulk = Bulk; remoteConfigs = configs; } if (cluster.isMaster) { - storeSet("cly_remote_configs", remoteConfigs); + CountlyStorage.storeSet("cly_remote_configs", remoteConfigs); cc.log(cc.logLevelEnums.INFO, `fetch_remote_config, Fetched remote config: [${remoteConfigs}].`); } } @@ -1352,7 +1346,7 @@ Countly.Bulk = Bulk; if (cluster.isMaster) { cc.log(cc.logLevelEnums.INFO, "request, Adding the raw request to the queue."); requestQueue.push(request); - storeSet("cly_queue", requestQueue); + CountlyStorage.storeSet("cly_queue", requestQueue); } else { cc.log(cc.logLevelEnums.INFO, "request, Sending message to the parent process. Adding the raw request to the queue."); @@ -1439,7 +1433,7 @@ Countly.Bulk = Bulk; cc.log(cc.logLevelEnums.INFO, "toRequestQueue, Adding request to the queue."); requestQueue.push(request); - storeSet("cly_queue", requestQueue); + CountlyStorage.storeSet("cly_queue", requestQueue); } else { cc.log(cc.logLevelEnums.INFO, "toRequestQueue, Sending message to the parent process. Adding request to the queue."); @@ -1470,7 +1464,7 @@ Countly.Bulk = Bulk; var events = eventQueue.splice(0, maxEventBatch); toRequestQueue({ events: JSON.stringify(events) }); } - storeSet("cly_event", eventQueue); + CountlyStorage.storeSet("cly_event", eventQueue); } // process request queue with event queue @@ -1485,12 +1479,12 @@ Countly.Bulk = Bulk; failTimeout = cc.getTimestamp() + failTimeoutAmount; cc.log(cc.logLevelEnums.ERROR, `makeRequest, Encountered a problem while making the request: [${err}]`); } - storeSet("cly_queue", requestQueue); + CountlyStorage.storeSet("cly_queue", requestQueue); readyToProcess = true; }, "heartBeat", false); } - setTimeout(heartBeat, beatInterval); + heartBeatTimer = setTimeout(heartBeat, beatInterval); } /** @@ -1748,138 +1742,6 @@ Countly.Bulk = Bulk; } } } - - /** - * Read value from file - * @param {String} key - key for file - * @returns {varies} value in file - */ - var readFile = function(key) { - var dir = path.resolve(__dirname, `${Countly.storage_path}__${key}.json`); - - // try reading data file - var data; - try { - data = fs.readFileSync(dir); - } - catch (ex) { - // there was no file, probably new init - data = null; - } - - try { - // trying to parse json string - data = JSON.parse(data); - } - catch (ex) { - // problem parsing, corrupted file? - cc.log(cc.logLevelEnums.ERROR, `readFile, Failed to parse the file with key: [${key}]. Error: [${ex}].`); - // backup corrupted file data - fs.writeFile(path.resolve(__dirname, `${Countly.storage_path}__${key}.${cc.getTimestamp()}${Math.random()}.json`), data, () => {}); - // start with new clean object - data = null; - } - return data; - }; - - /** - * Force store data synchronously on unrecoverable errors to preserve it for next launch - */ - var forceStore = function() { - for (var i in __data) { - var dir = path.resolve(__dirname, `${Countly.storage_path}__${i}.json`); - var ob = {}; - ob[i] = __data[i]; - try { - fs.writeFileSync(dir, JSON.stringify(ob)); - } - catch (ex) { - // tried to save whats possible - cc.log(cc.logLevelEnums.ERROR, `forceStore, Saving files failed. Error: [${ex}].`); - } - } - }; - - var asyncWriteLock = false; - var asyncWriteQueue = []; - - /** - * Write to file and process queue while in asyncWriteLock - * @param {String} key - key for value to store - * @param {varies} value - value to store - * @param {Function} callback - callback to call when done storing - */ - var writeFile = function(key, value, callback) { - var ob = {}; - ob[key] = value; - var dir = path.resolve(__dirname, `${Countly.storage_path}__${key}.json`); - fs.writeFile(dir, JSON.stringify(ob), (err) => { - if (err) { - cc.log(cc.logLevelEnums.ERROR, `writeFile, Writing files failed. Error: [${err}].`); - } - if (typeof callback === "function") { - callback(err); - } - if (asyncWriteQueue.length) { - setTimeout(() => { - var arr = asyncWriteQueue.shift(); - writeFile(arr[0], arr[1], arr[2]); - }, 0); - } - else { - asyncWriteLock = false; - } - }); - }; - - /** - * Save value in storage - * @param {String} key - key for value to store - * @param {varies} value - value to store - * @param {Function} callback - callback to call when done storing - */ - var storeSet = function(key, value, callback) { - __data[key] = value; - if (!asyncWriteLock) { - asyncWriteLock = true; - writeFile(key, value, callback); - } - else { - asyncWriteQueue.push([key, value, callback]); - } - }; - - /** - * Get value from storage - * @param {String} key - key of value to get - * @param {varies} def - default value to use if not set - * @returns {varies} value for the key - */ - var storeGet = function(key, def) { - cc.log(cc.logLevelEnums.DEBUG, `storeGet, Fetching item from storage with key: [${key}].`); - if (typeof __data[key] === "undefined") { - var ob = readFile(key); - var obLen; - // check if the 'read object' is empty or not - try { - obLen = Object.keys(ob).length; - } - catch (error) { - // if we can not even asses length set it to 0 so we can return the default value - obLen = 0; - } - - // if empty or falsy set default value - if (!ob || obLen === 0) { - __data[key] = def; - } - // else set the value read file has - else { - __data[key] = ob[key]; - } - } - return __data[key]; - }; }()); module.exports = Countly; diff --git a/package.json b/package.json index 4e7ff7a..c35b9f7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "countly-sdk-nodejs", - "version": "22.06.0", + "version": "24.10.0", "description": "Countly NodeJS SDK", "main": "lib/countly.js", "directories": { diff --git a/test/customStorageDirectory/.gitignore b/test/customStorageDirectory/.gitignore new file mode 100644 index 0000000..86d0cb2 --- /dev/null +++ b/test/customStorageDirectory/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore \ No newline at end of file diff --git a/test/helpers/helper_functions.js b/test/helpers/helper_functions.js index b87f825..d8340f6 100644 --- a/test/helpers/helper_functions.js +++ b/test/helpers/helper_functions.js @@ -1,47 +1,119 @@ /* eslint-disable no-unused-vars */ +/* eslint-disable no-console */ +/* global runthis */ var path = require("path"); var assert = require("assert"); var fs = require("fs"); +var fsp = require("fs/promises"); var Countly = require("../../lib/countly"); +var CountlyStorage = require("../../lib/countly-storage"); // paths for convenience var dir = path.resolve(__dirname, "../../"); -var idDir = (`${dir}/data/__cly_id.json`); -var eventDir = (`${dir}/data/__cly_event.json`); -var reqDir = (`${dir}/data/__cly_queue.json`); +var dir_test = path.resolve(__dirname, "../"); + +// paths for convenience +const DIR_CLY = (`${dir}/data`); +const DIR_CLY_ID = (`${dir}/data/__cly_id.json`); +const DIR_CLY_ID_type = (`${dir}/data/__cly_id_type.json`); +const DIR_CLY_event = (`${dir}/data/__cly_event.json`); +const DIR_CLY_request = (`${dir}/data/__cly_queue.json`); + +// Bulk paths for convenience +const DIR_Bulk = (`${dir}/bulk_data`); +const DIR_Bulk_bulk = (`${dir}/bulk_data/__cly_bulk_queue.json`); +const DIR_Bulk_event = (`${dir}/bulk_data/__cly_bulk_event.json`); +const DIR_Bulk_request = (`${dir}/bulk_data/__cly_req_queue.json`); + +// Custom +const DIR_Test = (`${dir_test}/customStorageDirectory`); +const DIR_Test_event = (`${dir_test}/customStorageDirectory/__cly_event.json`); +const DIR_Test_request = (`${dir_test}/customStorageDirectory/__cly_queue.json`); +const DIR_Test_bulk = (`${dir_test}/customStorageDirectory/__cly_bulk_queue.json`); +const DIR_Test_bulk_event = (`${dir_test}/customStorageDirectory/__cly_bulk_event.json`); +const DIR_Test_bulk_request = (`${dir_test}/customStorageDirectory/__cly_req_queue.json`); + // timeout variables -var sWait = 50; -var mWait = 3000; -var lWait = 10000; +const sWait = 50; +const mWait = 3000; +const lWait = 10000; + // parsing event queue -function readEventQueue() { - var a = JSON.parse(fs.readFileSync(eventDir, "utf-8")).cly_event; +function readEventQueue(givenPath = null, isBulk = false) { + var destination = DIR_CLY_event; + if (givenPath !== null) { + destination = givenPath; + } + var a = JSON.parse(fs.readFileSync(destination, "utf-8")).cly_event; + if (isBulk) { + a = JSON.parse(fs.readFileSync(destination, "utf-8")).cly_bulk_event; + } return a; } // parsing request queue -function readRequestQueue() { - var a = JSON.parse(fs.readFileSync(reqDir, "utf-8")).cly_queue; +function readRequestQueue(customPath = false, isBulk = false, isMemory = false) { + var destination = DIR_CLY_request; + if (customPath) { + destination = DIR_Test_request; + } + var a; + if (isBulk) { + a = JSON.parse(fs.readFileSync(destination, "utf-8")).cly_req_queue; + } + if (isMemory) { + a = CountlyStorage.storeGet("cly_queue"); + } + else { + a = JSON.parse(fs.readFileSync(destination, "utf-8")).cly_queue; + } return a; } +function doesFileStoragePathsExist(callback, isBulk = false, testPath = false) { + var paths = [DIR_CLY_ID, DIR_CLY_ID_type, DIR_CLY_event, DIR_CLY_request]; -// queue files clearing logic -function clearStorage(keepID) { - keepID = keepID || false; - // Resets Countly - Countly.halt(true); - // clean storages - if (fs.existsSync(eventDir)) { - fs.unlinkSync(eventDir); + if (isBulk) { + paths = [DIR_Bulk_request, DIR_Bulk_event, DIR_Bulk_bulk]; } - if (fs.existsSync(reqDir)) { - fs.unlinkSync(reqDir); + else if (testPath) { + paths = [DIR_Test_event, DIR_Test_request]; } - if (!keepID) { - if (fs.existsSync(idDir)) { - fs.unlinkSync(idDir); - } + + let errors = 0; + paths.forEach((p, index) => { + fs.access(p, fs.constants.F_OK, (err) => { + if (err) { + errors++; + } + if (index === p.length - 1) { + callback(errors === 0); + } + }); + }); +} +async function clearStorage(customPath = null) { + Countly.halt(true); + + const relativePath = `../${customPath}`; + const resolvedCustomPath = path.resolve(__dirname, relativePath); + + await fsp.rm(DIR_CLY, { recursive: true, force: true }).catch(() => { }); + await fsp.rm(DIR_Bulk, { recursive: true, force: true }).catch(() => { }); + await fsp.rm(DIR_Test, { recursive: true, force: true }).catch(() => { }); + + if (resolvedCustomPath !== null && typeof resolvedCustomPath === 'string') { + await fsp.rm(resolvedCustomPath, { recursive: true, force: true }).catch(() => { }); + } + + const storageExists = await fsp.access(DIR_CLY).then(() => true).catch(() => false); + const bulkStorageExists = await fsp.access(DIR_Bulk).then(() => true).catch(() => false); + const customTestStorage = await fsp.access(DIR_Test).then(() => true).catch(() => false); + const customStorageExists = resolvedCustomPath !== null ? await fsp.access(resolvedCustomPath).then(() => true).catch(() => false) : false; + + if (storageExists || bulkStorageExists || customTestStorage || customStorageExists) { + throw new Error("Failed to clear storage"); } } + /** * bunch of tests specifically gathered for testing events * @param {Object} eventObject - Original event object to test @@ -67,11 +139,16 @@ function eventValidator(eventObject, eventQueue, time) { } assert.equal(eventObject.dur, eventQueue.dur); } - // check if segmentation exists. If it is add test(s) + // check if segmentation exists. If it does, add tests if (typeof eventObject.segmentation !== 'undefined') { - // loop through segmentation keys and create tets + // loop through segmentation keys and create tests for (var key in eventObject.segmentation) { - assert.equal(eventObject.segmentation[key], eventQueue.segmentation[key]); + if (Array.isArray(eventObject.segmentation[key]) || typeof eventObject.segmentation[key] === 'object') { + assert.deepStrictEqual(eventObject.segmentation[key], eventQueue.segmentation[key]); + } + else { + assert.equal(eventObject.segmentation[key], eventQueue.segmentation[key]); + } } } // common parameter validation @@ -95,7 +172,7 @@ function requestBaseParamValidator(resultingObject, id) { assert.ok(typeof resultingObject.sdk_version !== 'undefined'); assert.ok(typeof resultingObject.timestamp !== 'undefined'); assert.ok(resultingObject.dow > -1 && resultingObject.dow < 24); - assert.ok(resultingObject.dow > 0 && resultingObject.dow < 8); + assert.ok(resultingObject.dow >= 0 && resultingObject.dow < 8); } /** * bunch of tests specifically gathered for testing crashes @@ -137,28 +214,41 @@ function sessionRequestValidator(beginSs, endSs, time, id) { assert.equal(time, endSs.session_duration); } } -/** - * bunch of tests specifically gathered for testing user details - * @param {Object} originalDetails - Original object that contains user details - * @param {Object} details - Object from cly_queue that corresponds to user details recording - */ -function userDetailRequestValidator(originalDetails, details) { - requestBaseParamValidator(details); - var user = JSON.parse(details.user_details); - assert.equal(originalDetails.name, user.name); - assert.equal(originalDetails.username, user.username); - assert.equal(originalDetails.email, user.email); - assert.equal(originalDetails.organization, user.organization); - assert.equal(originalDetails.phone, user.phone); - assert.equal(originalDetails.picture, user.picture); - assert.equal(originalDetails.gender, user.gender); - assert.equal(originalDetails.byear, user.byear); - if (typeof originalDetails.custom !== 'undefined') { - for (var key in originalDetails.custom) { - assert.equal(originalDetails.custom[key], user.custom[key]); + +function validateUserDetails(actual, expected) { + // Helper function to remove undefined values + const cleanObj = (obj) => { + if (typeof obj === "string") { + try { + // Parse if it's a JSON string + obj = JSON.parse(obj); + } + catch (e) { + console.error("Invalid JSON string:", obj); + // Return null for invalid JSON + return null; + } } + // Remove properties with undefined values + return Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== undefined)); + }; + const cleanedActual = cleanObj(actual); + const cleanedExpected = cleanObj(expected); + if (!cleanedActual || !cleanedExpected) { + // If either cleaned object is null, validation fails + return false; + } + // Perform deep strict comparison after cleaning up undefined values + try { + assert.deepStrictEqual(cleanedActual, cleanedExpected); + return true; + } + catch (e) { + console.log("Validation failed:", e); + return false; } } + /** * bunch of tests specifically gathered for testing page views * @param {Object} name - page name @@ -191,6 +281,7 @@ module.exports = { eventValidator, crashRequestValidator, sessionRequestValidator, - userDetailRequestValidator, + validateUserDetails, viewEventValidator, + doesFileStoragePathsExist, }; \ No newline at end of file diff --git a/test/helpers/test_utils.js b/test/helpers/test_utils.js new file mode 100644 index 0000000..ded2f53 --- /dev/null +++ b/test/helpers/test_utils.js @@ -0,0 +1,98 @@ +var eventObj = { + key: "event_check", + count: 55, + sum: 3.14, + dur: 2000, + segmentation: { + string_value: "example", + number_value: 42, + boolean_value: true, + array_value: ["item1", "item2"], + object_value: { nested_key: "nested_value" }, + null_value: null, + undefined_value: undefined, + }, +}; + +var timedEventObj = { + key: "timed", + count: 1, + segmentation: { + app_version: "1.0", + country: "Turkey", + }, +}; + +var userDetailObj = { + name: "Alexandrina Jovovich", + username: "alex_jov", + email: "alex.jov@example.com", + organization: "TechNova", + phone: "+987654321", + picture: "https://example.com/images/profile_alex.jpg", + gender: "Female", + byear: 1992, + custom: { + string_value: "example", + number_value: 42, + boolean_value: true, + array_value: ["item1", "item2"], + object_value: { nested_key: "nested_value" }, + }, +}; + +const invalidStorageMethods = { + _storage: {}, + setInvalid: function() { + }, + getInvalid: function() { + }, + removeInvalid: function() { + }, +}; + +const customStorage = { + _storage: {}, + storeSet: function(key, value, callback) { + if (key) { + this._storage[key] = value; + if (typeof callback === "function") { + callback(null); + } + } + }, + storeGet: function(key, def) { + return typeof this._storage[key] !== "undefined" ? this._storage[key] : def; + }, + storeRemove: function(key) { + delete this._storage[key]; + }, +}; + +function getInvalidStorage() { + return invalidStorageMethods; +} + +function getCustomStorage() { + return customStorage; +} + +var getUserDetailsObj = function() { + return userDetailObj; +}; + +var getEventObj = function() { + return eventObj; +}; + +var getTimedEventObj = function() { + return timedEventObj; +}; + +module.exports = { + getEventObj, + getUserDetailsObj, + getTimedEventObj, + getInvalidStorage, + getCustomStorage, +}; \ No newline at end of file diff --git a/test/tests_bulk.js b/test/tests_bulk.js new file mode 100644 index 0000000..d0a6724 --- /dev/null +++ b/test/tests_bulk.js @@ -0,0 +1,365 @@ +/* eslint-disable no-console */ +/* global runthis */ +const assert = require("assert"); +const CountlyBulk = require("../lib/countly-bulk"); +var hp = require("./helpers/helper_functions"); +var storage = require("../lib/countly-storage"); +var testUtils = require("./helpers/test_utils"); + +const { StorageTypes } = CountlyBulk; + +var appKey = "YOUR_APP_KEY"; +var serverUrl = "https://tests.url.cly"; + +function validateCrash(validator, nonfatal) { + assert.ok(validator.crash._os); + assert.ok(validator.crash._os_version); + assert.ok(validator.crash._error); + assert.ok(validator.crash._app_version); + assert.ok(typeof validator.crash._run !== 'undefined'); + assert.ok(typeof validator.crash._custom !== 'undefined'); + assert.equal(nonfatal, validator.crash._nonfatal); + assert.equal(true, validator.crash._javascript); + assert.equal(true, validator.crash._not_os_specific); +} + +// Create bulk data +function createBulkData(bulk) { + // Add an event + var user = bulk.add_user({ device_id: "testUser1" }); + user.add_event(testUtils.getEventObj()); + + // add user details + var user2 = bulk.add_user({ device_id: "testUser2" }); + user2.user_details(testUtils.getUserDetailsObj()); + + // add request + bulk.add_request({ device_id: "TestUser3" }); + + // add Crash + var user4 = bulk.add_user({ device_id: "TestUser4" }); + try { + runthis(); + } + catch (ex) { + user4.report_crash({ + _os: "Android", + _os_version: "7", + _error: "Stack trace goes here", + _app_version: "1.0", + _run: 12345, + _custom: {}, + _nonfatal: true, + _javascript: true, + _not_os_specific: true, + }, 1500645200); + } +} + +// Validate created bulk data +function validateCreatedBulkData(bulk) { + var events = bulk._getBulkEventQueue(); + var reqQueue = bulk._getBulkRequestQueue(); + var bulkQueue = bulk._getBulkQueue(); + + assert.equal(Object.keys(events).length, 1); + assert.equal(reqQueue.length, 3); + assert.equal(bulkQueue.length, 0); + + var deviceEvents = events.testUser1; // Access the events for the specific device + var recordedEvent = deviceEvents[0]; // Access the first event + hp.eventValidator(testUtils.getEventObj(), recordedEvent); + + var req = reqQueue[0]; // read user details queue + const actualUserDetails = req.user_details; // Extract the user_details from the actual request + const isValid = hp.validateUserDetails(actualUserDetails, testUtils.getUserDetailsObj()); + assert.equal(true, isValid); + + var testUser3Request = reqQueue.find((request) => request.device_id === "TestUser3"); + assert.ok(testUser3Request); + assert.strictEqual(testUser3Request.device_id, "TestUser3"); + assert.strictEqual(testUser3Request.app_key, "YOUR_APP_KEY"); + assert.strictEqual(testUser3Request.sdk_name, "javascript_native_nodejs_bulk"); + + var testUser4Request = reqQueue.find((request) => request.device_id === "TestUser4"); + validateCrash(testUser4Request, true); +} + +function shouldFilesExist(shouldExist, isCustomTest = false) { + hp.doesFileStoragePathsExist((exists) => { + assert.equal(shouldExist, exists); + }, true, isCustomTest); +} + +describe("Bulk Tests", () => { + beforeEach(async() => { + await hp.clearStorage(); + }); + // without any config option default bulk storage + // bulk mode is memory only by default + it("1- CNR", (done) => { + var bulk = new CountlyBulk({ + app_key: appKey, + url: serverUrl, + }); + assert.equal(storage.getStoragePath(), undefined); + shouldFilesExist(false); + createBulkData(bulk); + + setTimeout(() => { + validateCreatedBulkData(bulk); + shouldFilesExist(false); + done(); + }, hp.mWait); + }); + + // storage path and storage type provided in bulk + // type should be file and path should be correct + it("2- CNR_cPath_file", (done) => { + var bulk = new CountlyBulk({ + app_key: appKey, + url: serverUrl, + storage_path: "../test/customStorageDirectory/", + storage_type: StorageTypes.FILE, + }); + shouldFilesExist(true, true); + assert.equal(storage.getStoragePath(), "../test/customStorageDirectory/"); + createBulkData(bulk); + + setTimeout(() => { + validateCreatedBulkData(bulk); + shouldFilesExist(true, true); + assert.equal(storage.getStoragePath(), "../test/customStorageDirectory/"); + done(); + }, hp.sWait); + }); + + // file storage type in config + // path should become the default "../bulk_data/" + it("3- CNR_file", (done) => { + var bulk = new CountlyBulk({ + app_key: appKey, + url: serverUrl, + storage_type: StorageTypes.FILE, + }); + assert.equal(storage.getStoragePath(), "../bulk_data/"); + shouldFilesExist(true); + createBulkData(bulk); + + setTimeout(() => { + validateCreatedBulkData(bulk); + shouldFilesExist(true); + assert.equal(storage.getStoragePath(), "../bulk_data/"); + done(); + }, hp.mWait); + }); + + // memory storage type in config + // path should become undefined and storage files shouldn't exist + it("4- CNR_memory", (done) => { + var bulk = new CountlyBulk({ + app_key: appKey, + url: serverUrl, + storage_type: StorageTypes.MEMORY, + }); + assert.equal(storage.getStoragePath(), undefined); + shouldFilesExist(false); + createBulkData(bulk); + + setTimeout(() => { + validateCreatedBulkData(bulk); + shouldFilesExist(false); + assert.equal(storage.getStoragePath(), undefined); + done(); + }, hp.mWait); + }); + + // custom storage path and memory storage type provided in config + // path should become undefined, type should be memory storage + it("5- CNR_cPath_memory", (done) => { + var bulk = new CountlyBulk({ + app_key: appKey, + url: serverUrl, + storage_path: "../test/customStorageDirectory/", + storage_type: StorageTypes.MEMORY, + }); + assert.equal(storage.getStoragePath(), undefined); + shouldFilesExist(false); + createBulkData(bulk); + + setTimeout(() => { + validateCreatedBulkData(bulk); + shouldFilesExist(false); + assert.equal(storage.getStoragePath(), undefined); + done(); + }, hp.mWait); + }); + + // persist_queue is true in bulk config + // should switch to file storage and default bulk path + it("6- CNR_persistTrue", (done) => { + var bulk = new CountlyBulk({ + app_key: appKey, + url: serverUrl, + persist_queue: true, + }); + assert.equal(storage.getStoragePath(), "../bulk_data/"); + shouldFilesExist(true); + createBulkData(bulk); + + setTimeout(() => { + validateCreatedBulkData(bulk); + shouldFilesExist(true); + assert.equal(storage.getStoragePath(), "../bulk_data/"); + done(); + }, hp.mWait); + }); + + // persist_queue is false in bulk config + // should result same with default bulk configurations + it("7- CNR_persistFalse", (done) => { + var bulk = new CountlyBulk({ + app_key: appKey, + url: serverUrl, + persist_queue: false, + }); + assert.equal(storage.getStoragePath(), undefined); + shouldFilesExist(false); + createBulkData(bulk); + + setTimeout(() => { + validateCreatedBulkData(bulk); + shouldFilesExist(false); + assert.equal(storage.getStoragePath(), undefined); + done(); + }, hp.mWait); + }); + + // custom path is provided and persist_queue is true in config + // custom path should become the storage path and switch to file storage + it("8- CNR_cPath_persistTrue", (done) => { + var bulk = new CountlyBulk({ + app_key: appKey, + url: serverUrl, + storage_path: "../test/customStorageDirectory/", + persist_queue: true, + }); + assert.equal(storage.getStoragePath(), "../test/customStorageDirectory/"); + shouldFilesExist(true); + createBulkData(bulk); + + setTimeout(() => { + validateCreatedBulkData(bulk); + shouldFilesExist(true); + assert.equal(storage.getStoragePath(), "../test/customStorageDirectory/"); + done(); + }, hp.mWait); + }); + + // custom path is provided and persist_queue is false in config + // storage path should become undefined, should stay in memory storage + it("9- CNR_cPath_persistFalse", (done) => { + var bulk = new CountlyBulk({ + app_key: appKey, + url: serverUrl, + storage_path: "../test/customStorageDirectory/", + persist_queue: false, + }); + assert.equal(storage.getStoragePath(), undefined); + shouldFilesExist(false); + createBulkData(bulk); + + setTimeout(() => { + validateCreatedBulkData(bulk); + shouldFilesExist(false); + assert.equal(storage.getStoragePath(), undefined); + done(); + }, hp.mWait); + }); + + // persist_queue is true and storage type is file + // should directly switch to the default file storage for bulk + it("10- CNR_persistTrue_file", (done) => { + var bulk = new CountlyBulk({ + app_key: appKey, + url: serverUrl, + storage_type: StorageTypes.FILE, + persist_queue: true, + }); + assert.equal(storage.getStoragePath(), "../bulk_data/"); + shouldFilesExist(true); + createBulkData(bulk); + + setTimeout(() => { + validateCreatedBulkData(bulk); + shouldFilesExist(true); + assert.equal(storage.getStoragePath(), "../bulk_data/"); + done(); + }, hp.mWait); + }); + + // persist_queue is false and storage type is file + // storage type should overrule and switch into the default bulk file storage + it("11- CNR_persistFalse_file", (done) => { + var bulk = new CountlyBulk({ + app_key: appKey, + url: serverUrl, + storage_type: StorageTypes.FILE, + persist_queue: true, + }); + assert.equal(storage.getStoragePath(), "../bulk_data/"); + shouldFilesExist(true); + createBulkData(bulk); + + setTimeout(() => { + validateCreatedBulkData(bulk); + shouldFilesExist(true); + assert.equal(storage.getStoragePath(), "../bulk_data/"); + done(); + }, hp.mWait); + }); + + // persist_queue is true and storage type is file and custom path is given + // storage type should overrule and switch into the custom path file storage + it("12- CNR_cPath_persistTrue_file", (done) => { + var bulk = new CountlyBulk({ + app_key: appKey, + url: serverUrl, + storage_type: StorageTypes.FILE, + storage_path: "../test/customStorageDirectory/", + persist_queue: true, + }); + assert.equal(storage.getStoragePath(), "../test/customStorageDirectory/"); + shouldFilesExist(true); + createBulkData(bulk); + + setTimeout(() => { + validateCreatedBulkData(bulk); + shouldFilesExist(true); + assert.equal(storage.getStoragePath(), "../test/customStorageDirectory/"); + done(); + }, hp.mWait); + }); + + // persist_queue is false and storage type is file and custom path is given + // storage type should overrule and switch into the custom path file storage + it("13- CNR_cPath_persistFalse_file", (done) => { + var bulk = new CountlyBulk({ + app_key: appKey, + url: serverUrl, + storage_type: StorageTypes.FILE, + storage_path: "../test/customStorageDirectory/", + persist_queue: false, + }); + assert.equal(storage.getStoragePath(), "../test/customStorageDirectory/"); + shouldFilesExist(true); + createBulkData(bulk); + + setTimeout(() => { + validateCreatedBulkData(bulk); + shouldFilesExist(true); + assert.equal(storage.getStoragePath(), "../test/customStorageDirectory/"); + done(); + }, hp.mWait); + }); +}); \ No newline at end of file diff --git a/test/tests_consents.js b/test/tests_consents.js index 057f5d7..a89b4e1 100644 --- a/test/tests_consents.js +++ b/test/tests_consents.js @@ -62,8 +62,10 @@ function events() { // tests describe("Internal event consent tests", () => { + beforeEach(async() => { + await hp.clearStorage(); + }); it("Only custom event should be sent to the queue", (done) => { - hp.clearStorage(); initMain(); Countly.add_consent(["events"]); events(); @@ -75,7 +77,6 @@ describe("Internal event consent tests", () => { }, hp.sWait); }); it("All but custom event should be sent to the queue", (done) => { - hp.clearStorage(); initMain(); Countly.add_consent(["sessions", "views", "users", "star-rating", "apm", "feedback"]); events(); @@ -91,7 +92,6 @@ describe("Internal event consent tests", () => { }, hp.mWait); }); it("Non-merge ID change should reset all consents", (done) => { - hp.clearStorage(); initMain(); Countly.add_consent(["sessions", "views", "users", "star-rating", "apm", "feedback"]); Countly.change_id("Richard Wagner II", false); @@ -102,7 +102,6 @@ describe("Internal event consent tests", () => { }, hp.sWait); }); it("Merge ID change should not reset consents", (done) => { - hp.clearStorage(); initMain(); Countly.add_consent(["sessions", "views", "users", "star-rating", "apm", "feedback"]); // Countly.change_id("Richard Wagner the second", true); diff --git a/test/tests_crashes.js b/test/tests_crashes.js index 2adedb5..12f2688 100644 --- a/test/tests_crashes.js +++ b/test/tests_crashes.js @@ -14,9 +14,10 @@ function initMain() { } describe("Crash tests", () => { + before(async() => { + await hp.clearStorage(); + }); it("Validate handled error logic", (done) => { - // clear previous data - hp.clearStorage(); // initialize SDK initMain(); // error logic @@ -34,23 +35,25 @@ describe("Crash tests", () => { done(); }, hp.sWait); }); - // This needs two steps, first creating an error and second checking the logs without erasing, otherwise error would halt the test - describe("Unhandled Error logic", () => { - it("Create unhandled rejection", () => { - // clear previous data - hp.clearStorage(); - // initialize SDK - initMain(); - // send emitter - Countly.track_errors(); - process.emit('unhandledRejection'); - }); - it("Validate unhandled rejection recording", (done) => { - setTimeout(() => { - var req = hp.readRequestQueue()[0]; - hp.crashRequestValidator(req, false); - done(); - }, hp.mWait); - }); +}); +// This needs two steps, first creating an error and second checking the logs without erasing, otherwise error would halt the test +describe("Unhandled Error logic", () => { + before(async() => { + await hp.clearStorage(); + }); + it("Create unhandled rejection", (done) => { + // initialize SDK + initMain(); + // send emitter + Countly.track_errors(); + process.emit('unhandledRejection'); + done(); + }); + it("Validate unhandled rejection recording", (done) => { + setTimeout(() => { + var req = hp.readRequestQueue()[0]; + hp.crashRequestValidator(req, false); + done(); + }, hp.mWait); }); }); diff --git a/test/tests_device_id_type.js b/test/tests_device_id_type.js index afa76f4..650f1b5 100644 --- a/test/tests_device_id_type.js +++ b/test/tests_device_id_type.js @@ -55,9 +55,10 @@ function checkRequestsForT(queue, expectedInternalType) { } describe("Device ID type tests", () => { + beforeEach(async() => { + await hp.clearStorage(); + }); it("1.Generated device ID", (done) => { - // clear previous data - hp.clearStorage(); // initialize SDK initMain(undefined); Countly.begin_session(); @@ -71,8 +72,6 @@ describe("Device ID type tests", () => { }, hp.sWait); }); it("2.Developer supplied device ID", (done) => { - // clear previous data - hp.clearStorage(); // initialize SDK initMain("ID"); Countly.begin_session(); @@ -86,8 +85,6 @@ describe("Device ID type tests", () => { }, hp.sWait); }); it("3.With stored dev ID and no new ID", (done) => { - // clear previous data - hp.clearStorage(); // initialize SDK initMain("ID"); Countly.begin_session(); @@ -109,8 +106,6 @@ describe("Device ID type tests", () => { }, hp.sWait); }); it("4.With stored dev ID and with new ID", (done) => { - // clear previous data - hp.clearStorage(); // initialize SDK initMain("ID"); Countly.begin_session(); @@ -132,8 +127,6 @@ describe("Device ID type tests", () => { }, hp.sWait); }); it("5.With stored generated ID and no new ID", (done) => { - // clear previous data - hp.clearStorage(); // initialize SDK initMain(undefined); Countly.begin_session(); @@ -157,8 +150,6 @@ describe("Device ID type tests", () => { }, hp.sWait); }); it("6.With stored generated ID and with new ID", (done) => { - // clear previous data - hp.clearStorage(); // initialize SDK initMain(undefined); Countly.begin_session(); @@ -182,8 +173,6 @@ describe("Device ID type tests", () => { }, hp.sWait); }); it("7.With stored dev ID and no new ID, flag set", (done) => { - // clear previous data - hp.clearStorage(); // initialize SDK initMain("ID"); Countly.begin_session(); @@ -206,31 +195,29 @@ describe("Device ID type tests", () => { }); it("8.With stored dev ID and with new ID, flag set", (done) => { - // clear previous data - hp.clearStorage(); - // initialize SDK - initMain("ID"); - Countly.begin_session(); - // read request queue setTimeout(() => { - var rq = hp.readRequestQueue()[0]; - assert.equal(Countly.get_device_id(), "ID"); - assert.equal(Countly.get_device_id_type(), cc.deviceIdTypeEnums.DEVELOPER_SUPPLIED); - checkRequestsForT(rq, cc.deviceIdTypeEnums.DEVELOPER_SUPPLIED); - Countly.halt(true); - initMain("ID2", true); + // initialize SDK + initMain("ID"); + Countly.begin_session(); + // read request queue setTimeout(() => { - var req = hp.readRequestQueue()[0]; - assert.equal(Countly.get_device_id(), "ID2"); + var rq = hp.readRequestQueue()[0]; + assert.equal(Countly.get_device_id(), "ID"); assert.equal(Countly.get_device_id_type(), cc.deviceIdTypeEnums.DEVELOPER_SUPPLIED); - checkRequestsForT(req, cc.deviceIdTypeEnums.DEVELOPER_SUPPLIED); - done(); - }, hp.sWait); - }, hp.sWait); + checkRequestsForT(rq, cc.deviceIdTypeEnums.DEVELOPER_SUPPLIED); + Countly.halt(true); + initMain("ID2", true); + setTimeout(() => { + var req = hp.readRequestQueue()[0]; + assert.equal(Countly.get_device_id(), "ID2"); + assert.equal(Countly.get_device_id_type(), cc.deviceIdTypeEnums.DEVELOPER_SUPPLIED); + checkRequestsForT(req, cc.deviceIdTypeEnums.DEVELOPER_SUPPLIED); + done(); + }, hp.sWait); + }, hp.lWait); + }, hp.lWait); }); it("9.With stored sdk ID and no new ID, flag set", (done) => { - // clear previous data - hp.clearStorage(); // initialize SDK initMain(undefined); Countly.begin_session(); @@ -255,8 +242,6 @@ describe("Device ID type tests", () => { }); it("10.With stored sdk ID and with new ID, flag set", (done) => { - // clear previous data - hp.clearStorage(); // initialize SDK initMain(undefined); Countly.begin_session(); @@ -280,8 +265,6 @@ describe("Device ID type tests", () => { }, hp.sWait); }); it("11.Change generated device ID", (done) => { - // clear previous data - hp.clearStorage(); // initialize SDK initMain(undefined); Countly.change_id("changedID"); @@ -298,8 +281,6 @@ describe("Device ID type tests", () => { }, hp.sWait); }); it("12.Change developer supplied device ID", (done) => { - // clear previous data - hp.clearStorage(); // initialize SDK initMain("ID"); Countly.change_id("changedID"); diff --git a/test/tests_events.js b/test/tests_events.js index 9e93305..127c4d5 100644 --- a/test/tests_events.js +++ b/test/tests_events.js @@ -1,6 +1,7 @@ /* eslint-disable no-console */ var Countly = require("../lib/countly"); var hp = require("./helpers/helper_functions"); +var testUtils = require("./helpers/test_utils"); // init function function initMain() { @@ -11,54 +12,34 @@ function initMain() { max_events: -1, }); } -// an event object to use -var eventObj = { - key: "in_app_purchase", - count: 3, - sum: 2.97, - dur: 1000, - segmentation: { - app_version: "1.0", - country: "Turkey", - }, -}; -// a timed event object -var timedEventObj = { - key: "timed", - count: 1, - segmentation: { - app_version: "1.0", - country: "Turkey", - }, -}; + describe("Events tests", () => { + beforeEach(async() => { + await hp.clearStorage(); + }); it("Record and check custom event", (done) => { - // clear previous data - hp.clearStorage(); // initialize SDK initMain(); // send custom event - Countly.add_event(eventObj); + Countly.add_event(testUtils.getEventObj()); // read event queue setTimeout(() => { var event = hp.readEventQueue()[0]; - hp.eventValidator(eventObj, event); + hp.eventValidator(testUtils.getEventObj(), event); done(); }, hp.mWait); }); it("Record and check timed events", (done) => { - // clear previous data - hp.clearStorage(); // initialize SDK initMain(); // send timed event Countly.start_event("timed"); setTimeout(() => { - Countly.end_event(timedEventObj); + Countly.end_event(testUtils.getTimedEventObj()); // read event queue setTimeout(() => { var event = hp.readEventQueue()[0]; - hp.eventValidator(timedEventObj, event, (hp.mWait / 1000)); + hp.eventValidator(testUtils.getTimedEventObj(), event, (hp.mWait / 1000)); done(); }, hp.sWait); }, hp.mWait); diff --git a/test/tests_internal_limits.js b/test/tests_internal_limits.js index 5d38307..82f0a10 100644 --- a/test/tests_internal_limits.js +++ b/test/tests_internal_limits.js @@ -22,6 +22,9 @@ function initLimitsMain() { // Integration tests with countly initialized describe("Testing internal limits", () => { + beforeEach(async() => { + await hp.clearStorage(); + }); describe("Testing truncation functions", () => { it("truncateSingleValue: Check if the string is truncated", () => { var newStr = cc.truncateSingleValue("123456789", 3, "test"); @@ -56,8 +59,6 @@ describe("Testing internal limits", () => { }); it("1. Check custom event truncation", (done) => { - // clear storage - hp.clearStorage(); // init Countly initLimitsMain(); // send event @@ -79,16 +80,15 @@ describe("Testing internal limits", () => { assert.ok(event.segmentation["key of 3"]); assert.ok(!event.segmentation["key of 4"]); assert.equal(event.segmentation["key of 3"], "Value of"); - assert.ok(event.timestamp); - assert.ok(event.hour); - assert.ok(event.dow); + // common parameter validation + assert.ok(event.timestamp !== 'undefined'); + assert.ok(event.hour !== 'undefined'); + assert.ok(event.dow !== 'undefined'); done(); }, hp.sWait); }); it("2. Check countly view event truncation", (done) => { - // clear storage - hp.clearStorage(); // init Countly initLimitsMain(); // page view @@ -101,15 +101,14 @@ describe("Testing internal limits", () => { assert.equal(event.segmentation.name, "a very l"); assert.equal(event.segmentation.visit, 1); assert.ok(event.segmentation.segment); - assert.ok(event.timestamp); - assert.ok(event.hour); - assert.ok(event.dow); + // common parameter validation + assert.ok(event.timestamp !== 'undefined'); + assert.ok(event.hour !== 'undefined'); + assert.ok(event.dow !== 'undefined'); done(); }, hp.sWait); }); it("3. Check breadcrumbs and error truncation", (done) => { - // clear storage - hp.clearStorage(); // init Countly initLimitsMain(); // add log @@ -134,9 +133,10 @@ describe("Testing internal limits", () => { assert.ok(req.device_id); assert.ok(req.sdk_name); assert.ok(req.sdk_version); - assert.ok(req.timestamp); - assert.ok(req.hour); - assert.ok(req.dow); + // common parameter validation + assert.ok(req.timestamp !== 'undefined'); + assert.ok(req.hour !== 'undefined'); + assert.ok(req.dow !== 'undefined'); var crash = JSON.parse(req.crash); assert.equal(crash._logs, "log5 too\nlog6\nlog7"); assert.ok(crash._os); @@ -151,8 +151,6 @@ describe("Testing internal limits", () => { }, hp.sWait); }); it("4. Check user details truncation", (done) => { - // clear storage - hp.clearStorage(); // init Countly initLimitsMain(); // add user details @@ -184,9 +182,10 @@ describe("Testing internal limits", () => { assert.ok(req.device_id); assert.ok(req.sdk_name); assert.ok(req.sdk_version); - assert.ok(req.timestamp); - assert.ok(req.hour); - assert.ok(req.dow); + // common parameter validation + assert.ok(req.timestamp !== 'undefined'); + assert.ok(req.hour !== 'undefined'); + assert.ok(req.dow !== 'undefined'); var details = JSON.parse(req.user_details); assert.equal(details.name, 'Gottlob '); assert.equal(details.username, 'Grundges'); @@ -205,8 +204,6 @@ describe("Testing internal limits", () => { }, hp.sWait); }); it("5. Check custom properties truncation", (done) => { - // clear storage - hp.clearStorage(); // init Countly initLimitsMain(); // add custom properties @@ -229,9 +226,10 @@ describe("Testing internal limits", () => { assert.ok(req.device_id); assert.ok(req.sdk_name); assert.ok(req.sdk_version); - assert.ok(req.timestamp); - assert.ok(req.hour); - assert.ok(req.dow); + // common parameter validation + assert.ok(req.timestamp !== 'undefined'); + assert.ok(req.hour !== 'undefined'); + assert.ok(req.dow !== 'undefined'); var details = JSON.parse(req.user_details).custom; // set assert.equal(details['name of '], 'Bertrand'); diff --git a/test/tests_sessions.js b/test/tests_sessions.js index 2893cc7..ac17f3b 100644 --- a/test/tests_sessions.js +++ b/test/tests_sessions.js @@ -11,9 +11,10 @@ function initMain() { }); } describe("Sessions tests", () => { + beforeEach(async() => { + await hp.clearStorage(); + }); it("Start session and validate the request queue", (done) => { - // clear previous data - hp.clearStorage(); // initialize SDK initMain(); // send session calls @@ -25,8 +26,6 @@ describe("Sessions tests", () => { }, hp.sWait); }); it("Start and end session and validate the request queue", (done) => { - // clear previous data - hp.clearStorage(); // initialize SDK initMain(); // send session calls diff --git a/test/tests_storage.js b/test/tests_storage.js new file mode 100644 index 0000000..2e3c85e --- /dev/null +++ b/test/tests_storage.js @@ -0,0 +1,425 @@ +/* eslint-disable no-console */ +/* global runthis */ +const assert = require("assert"); +var Countly = require("../lib/countly"); +var storage = require("../lib/countly-storage"); +var hp = require("./helpers/helper_functions"); +var testUtils = require("./helpers/test_utils"); + +var appKey = "YOUR_APP_KEY"; +var serverUrl = "https://your.server.ly"; + +const { StorageTypes } = Countly; + +function shouldFilesExist(shouldExist, isCustomTest = false) { + hp.doesFileStoragePathsExist((exists) => { + assert.equal(shouldExist, exists); + }, false, isCustomTest); +} + +function validateStorageMethods() { + storage.storeSet("cly_count", 42); + storage.storeSet("cly_object", { key: "value" }); + storage.storeSet("cly_null", null); + + // Retrieve and assert values + assert.equal(storage.storeGet("cly_count"), 42); + assert.deepEqual(storage.storeGet("cly_object"), { key: "value" }); + assert.equal(storage.storeGet("cly_null"), null); +} + +function validateStorageTypeAndPath(expectedStorageType, isCustomPath = false) { + if (expectedStorageType === StorageTypes.FILE) { + if (!isCustomPath) { + assert.equal(storage.getStoragePath(), "../data/"); + assert.equal(storage.getStorageType(), StorageTypes.FILE); + } + else { + assert.equal(storage.getStoragePath(), "../test/customStorageDirectory/"); + assert.equal(storage.getStorageType(), StorageTypes.FILE); + } + } + else if (expectedStorageType === StorageTypes.MEMORY) { + assert.equal(storage.getStoragePath(), undefined); + assert.equal(storage.getStorageType(), StorageTypes.MEMORY); + } + // get storage type returns null in case of a custom method + else if (expectedStorageType === null) { + if (!isCustomPath) { + assert.equal(storage.getStoragePath(), "../data/"); + assert.equal(storage.getStorageType(), null); + } + else { + assert.equal(storage.getStoragePath(), "../test/customStorageDirectory/"); + assert.equal(storage.getStorageType(), null); + } + } +} +function createData() { + // begin a session + Countly.begin_session(true); + // add an event + Countly.add_event(testUtils.getEventObj()); + // add user details + Countly.user_details(testUtils.getUserDetailsObj()); + // add crash + Countly.track_errors(); + try { + runthis(); + } + catch (ex) { + Countly.log_error(ex); + } +} +function validateData(isCustomPath = false, isMemoryOrCustom = false) { + var beg = hp.readRequestQueue(isCustomPath, false, isMemoryOrCustom)[0]; + hp.sessionRequestValidator(beg); + + var ud = hp.readRequestQueue(isCustomPath, false, isMemoryOrCustom)[1]; + const isValid = hp.validateUserDetails(ud.user_details, testUtils.getUserDetailsObj()); + assert.equal(isValid, true); + + var crash = hp.readRequestQueue(isCustomPath, false, isMemoryOrCustom)[2]; + hp.crashRequestValidator(crash, true); + + var ev = hp.readRequestQueue(isCustomPath, false, isMemoryOrCustom)[3]; + var eventsArray = JSON.parse(ev.events); + hp.eventValidator(testUtils.getEventObj(), eventsArray[0]); +} +/* ++---------------------------------------------------+-------------------+ +| Configuration Option | Tested? (+/-) | ++---------------------------------------------------+-------------------+ +| 1. No Configuration Option Provided | + | ++---------------------------------------------------+-------------------+ +| 2. File Storage with Custom Path | + | ++---------------------------------------------------+-------------------+ +| 3. File Storage with Invalid Path | + | ++---------------------------------------------------+-------------------+ +| 4. File Storage while Custom Method Provided | + | ++---------------------------------------------------+-------------------+ +| 5. Memory Storage with No Path | + | ++---------------------------------------------------+-------------------+ +| 6. Memory Storage with Custom Path | + | ++---------------------------------------------------+-------------------+ +| 7. Memory Storage while Custom Method Provided | + | ++---------------------------------------------------+-------------------+ +| 8. Custom Storage Methods with No Path | + | ++---------------------------------------------------+-------------------+ +| 9. Custom Storage Methods with Custom Path | + | ++---------------------------------------------------+-------------------+ +| 10. Custom Storage with Invalid Path | + | ++---------------------------------------------------+-------------------+ +| 11. Custom Storage Methods with Invalid Methods | + | ++---------------------------------------------------+-------------------+ +| 12. Init Storage default no SDK init | + | ++---------------------------------------------------+-------------------+ +| 13. File Storage with null path no SDK init | + | ++---------------------------------------------------+-------------------+ +| 14. File Storage with custom path no SDK init | + | ++---------------------------------------------------+-------------------+ +| 15. Memory Storage without no SDK init | + | ++---------------------------------------------------+-------------------+ +| 16. Custom Storage with null path no SDK init | + | ++---------------------------------------------------+-------------------+ +| 17. Custom Storage with custom path no SDK init | + | ++---------------------------------------------------+-------------------+ +*/ + +describe("Storage Tests", () => { + beforeEach(async() => { + await hp.clearStorage(); + }); + + // if no config option provided sdk should init storage with default settings + // "../data/" as the storage path, FILE as the storage type + it("1- noConfigOption", (done) => { + Countly.init({ + app_key: appKey, + url: serverUrl, + }); + shouldFilesExist(true); + validateStorageTypeAndPath(StorageTypes.FILE); + createData(); + + setTimeout(() => { + shouldFilesExist(true); + validateStorageTypeAndPath(StorageTypes.FILE); + validateData(); + done(); + }, hp.mWait); + }); + + // if custom path is provided sdk should init storage with using that path + it("2- file_cPath", (done) => { + Countly.init({ + app_key: appKey, + url: serverUrl, + storage_path: "../test/customStorageDirectory/", + storage_type: StorageTypes.FILE, + }); + shouldFilesExist(true); + validateStorageTypeAndPath(StorageTypes.FILE, true); + createData(); + + setTimeout(() => { + shouldFilesExist(true); + validateStorageTypeAndPath(StorageTypes.FILE, true); + validateData(true); + done(); + }, hp.mWait); + }); + + // if invalid path is provided such as null or undefined sdk should init storage with default path + // validateStorageTypeAndPath checks path as "../data/" if true is not passed as second param + it("3- file_invalid_path", (done) => { + Countly.init({ + app_key: appKey, + url: serverUrl, + storage_path: undefined, + storage_type: StorageTypes.FILE, + }); + shouldFilesExist(true); + validateStorageTypeAndPath(StorageTypes.FILE); + createData(); + + setTimeout(() => { + shouldFilesExist(true); + validateStorageTypeAndPath(StorageTypes.FILE); + validateData(); + done(); + }, hp.mWait); + }); + + // since a custom method is provided sdk will switch to using that + // custom method will be applied, storage type will be null but sdk will create storage files anyway + it("4- file_cMethod", (done) => { + Countly.init({ + app_key: appKey, + url: serverUrl, + storage_type: StorageTypes.FILE, + custom_storage_method: testUtils.getCustomStorage(), + }); + shouldFilesExist(true); + // storage type will be null since custom method is provided + validateStorageTypeAndPath(null); + createData(); + + setTimeout(() => { + shouldFilesExist(true); + validateStorageTypeAndPath(null); + validateData(false, true); + done(); + }, hp.mWait); + }); + + // storage type will become memory, and storage files will not exist + it("5- memory_noPath", (done) => { + Countly.init({ + app_key: appKey, + url: serverUrl, + storage_type: StorageTypes.MEMORY, + }); + shouldFilesExist(false); + validateStorageTypeAndPath(StorageTypes.MEMORY); + createData(); + + setTimeout(() => { + shouldFilesExist(false); + validateStorageTypeAndPath(StorageTypes.MEMORY); + validateData(false, true); + done(); + }, hp.mWait); + }); + + // storage type will become memory, and storage files will not exist + // passing storage path will not affect how storage will initialize + it("6- memory_cPath", (done) => { + Countly.init({ + app_key: appKey, + url: serverUrl, + storage_type: StorageTypes.MEMORY, + storage_path: "../test/customStorageDirectory/", + }); + shouldFilesExist(false); + validateStorageTypeAndPath(StorageTypes.MEMORY); + createData(); + + setTimeout(() => { + shouldFilesExist(false); + validateStorageTypeAndPath(StorageTypes.MEMORY); + validateData(false, true); + done(); + }, hp.mWait); + }); + + // if custom method is provided with memory storage type, sdk will switch to custom method + it("7- memory_cMethod", (done) => { + Countly.init({ + app_key: appKey, + url: serverUrl, + storage_type: StorageTypes.MEMORY, + custom_storage_method: testUtils.getCustomStorage(), + }); + shouldFilesExist(false); + // storage type will be null since custom method is provided + validateStorageTypeAndPath(null); + createData(); + + setTimeout(() => { + shouldFilesExist(false); + validateStorageTypeAndPath(null); + validateData(false, true); + done(); + }, hp.mWait); + }); + + // custom method is provided without any path or storage type information + // storage files will be created and will set the path as the default but sdk will use custom methods + it("8- custom_noPath", (done) => { + Countly.init({ + app_key: appKey, + url: serverUrl, + custom_storage_method: testUtils.getCustomStorage(), + }); + shouldFilesExist(true); + // storage type will be null since custom method is provided + validateStorageTypeAndPath(null); + createData(); + + setTimeout(() => { + shouldFilesExist(true); + validateStorageTypeAndPath(null); + validateData(false, true); + done(); + }, hp.mWait); + }); + + // custom method is provided with custom path + // sdk will set the path as the custom and sdk will use the custom methods + it("9- custom_cPath", (done) => { + Countly.init({ + app_key: appKey, + url: serverUrl, + custom_storage_method: testUtils.getCustomStorage(), + storage_path: "../test/customStorageDirectory/", + }); + shouldFilesExist(true); + // storage type will be null since custom method is provided + // path will be the custom path in this case + validateStorageTypeAndPath(null, "../test/customStorageDirectory/"); + createData(); + + setTimeout(() => { + shouldFilesExist(true); + validateStorageTypeAndPath(null, "../test/customStorageDirectory/"); + validateData(false, true); + done(); + }, hp.mWait); + }); + + // custom storage method with invalid storage path + // sdk will not try to initialize the storage with invalid path and return to the default path + // storage files will exist, type will be null and use custom methods + it("10- custom_invalid_path", (done) => { + Countly.init({ + app_key: appKey, + url: serverUrl, + custom_storage_method: testUtils.getCustomStorage(), + storage_path: undefined, + }); + shouldFilesExist(true); + // storage type will be null since custom method is provided + validateStorageTypeAndPath(null); + createData(); + + setTimeout(() => { + shouldFilesExist(true); + validateStorageTypeAndPath(null); + validateData(false, true); + done(); + }, hp.mWait); + }); + + // custom storage method with invalid methods + // sdk should not use the invalid methods and switch back to file storage + it("11- custom_invalid_cMethod", (done) => { + Countly.init({ + app_key: appKey, + url: serverUrl, + custom_storage_method: testUtils.getInvalidStorage(), + }); + shouldFilesExist(true); + // storage type will be null since custom method is provided + validateStorageTypeAndPath(StorageTypes.FILE); + createData(); + + setTimeout(() => { + shouldFilesExist(true); + validateStorageTypeAndPath(StorageTypes.FILE); + validateData(); + done(); + }, hp.mWait); + }); + + // initStorage method without any parameters + // storage should initialize correctly and be ready to use + it("12- initStorage_noParams_noInit", (done) => { + storage.initStorage(); + shouldFilesExist(true); + // storage type will be File since it's default + validateStorageTypeAndPath(StorageTypes.FILE); + validateStorageMethods(); + done(); + }); + + // initStorage method with File storage type and null path + // storage should initialize correctly with default path + it("13- initStorage_file_nullPath_noInit", (done) => { + storage.initStorage(null, StorageTypes.FILE); + shouldFilesExist(true); + // storage type will be File since it's default + validateStorageTypeAndPath(StorageTypes.FILE); + validateStorageMethods(); + done(); + }); + + // initStorage method with File storage type and custom path + // storage should initialize correctly with custom path + it("14- initStorage_file_cPath_noInit", (done) => { + storage.initStorage("../test/customStorageDirectory/", StorageTypes.FILE); + shouldFilesExist(true); + // storage type will be File since it's default + validateStorageTypeAndPath(StorageTypes.FILE, true); + validateStorageMethods(); + done(); + }); + + // initStorage method with memory storage type and null path + // storage should initialize correctly with memory storage + it("15- initStorage_memory_noInit", (done) => { + storage.initStorage(null, StorageTypes.MEMORY); + shouldFilesExist(false); + validateStorageTypeAndPath(StorageTypes.MEMORY); + validateStorageMethods(); + done(); + }); + + // initStorage method with custom storage method and null path + it("16- initStorage_custom_nullPath_noInit", (done) => { + storage.initStorage(null, null, false, testUtils.getCustomStorage()); + shouldFilesExist(true); + validateStorageTypeAndPath(null); + validateStorageMethods(); + done(); + }); + + // initStorage method with custom storage method and custom path + it("17- initStorage_custom_cPath_noInit", (done) => { + storage.initStorage("../test/customStorageDirectory/", null, false, testUtils.getCustomStorage()); + shouldFilesExist(true); + validateStorageTypeAndPath(null, true); + validateStorageMethods(); + done(); + }); +}); \ No newline at end of file diff --git a/test/tests_user_details.js b/test/tests_user_details.js index 9e859ec..11b57b2 100644 --- a/test/tests_user_details.js +++ b/test/tests_user_details.js @@ -1,43 +1,26 @@ /* eslint-disable no-console */ +const assert = require("assert"); var Countly = require("../lib/countly"); var hp = require("./helpers/helper_functions"); - -var userDetailObj = { - name: "Barturiana Sosinsiava", - username: "bar2rawwen", - email: "test@test.com", - organization: "Dukely", - phone: "+123456789", - picture: "https://ps.timg.com/profile_images/52237/011_n_400x400.jpg", - gender: "Non-binary", - byear: 1987, // birth year - custom: { - "key1 segment": "value1 segment", - "key2 segment": "value2 segment", - }, -}; -// init function -function initMain() { - Countly.init({ - app_key: "YOUR_APP_KEY", - url: "https://try.count.ly", - interval: 10000, - max_events: -1, - }); -} +var testUtils = require("./helpers/test_utils"); describe("User details tests", () => { + beforeEach(async() => { + await hp.clearStorage(); + }); it("Record and validate all user details", (done) => { - // clear previous data - hp.clearStorage(); - // initialize SDK - initMain(); - // send user details + Countly.init({ + app_key: "YOUR_APP_KEY", + url: "https://try.count.ly", + }); + var userDetailObj = testUtils.getUserDetailsObj(); Countly.user_details(userDetailObj); // read event queue setTimeout(() => { var req = hp.readRequestQueue()[0]; - hp.userDetailRequestValidator(userDetailObj, req); + const actualUserDetails = req.user_details; + const isValid = hp.validateUserDetails(actualUserDetails, userDetailObj); + assert.equal(true, isValid); done(); }, hp.sWait); }); diff --git a/test/tests_views.js b/test/tests_views.js index 664f3ca..07e6926 100644 --- a/test/tests_views.js +++ b/test/tests_views.js @@ -15,9 +15,10 @@ function initMain() { } describe("View test", () => { + beforeEach(async() => { + await hp.clearStorage(); + }); it("Record and validate page views", (done) => { - // clear previous data - hp.clearStorage(); // initialize SDK initMain(); // send track view @@ -30,8 +31,6 @@ describe("View test", () => { }, hp.sWait); }); it("Record and validate timed page views with same name", (done) => { - // clear previous data - hp.clearStorage(); // initialize SDK initMain(); Countly.track_view(pageNameOne); @@ -52,8 +51,6 @@ describe("View test", () => { }, hp.mWait); }); it("Record and validate timed page views with same name", (done) => { - // clear previous data - hp.clearStorage(); // initialize SDK initMain(); Countly.track_view(pageNameOne);