|
| 1 | +/** |
| 2 | + * SDK behavior settings: checks the settings the server sends (from SDK Manager) or the developer provides, |
| 3 | + * and decides the value of each setting the SDK uses. |
| 4 | + * @module lib/countly-behavior |
| 5 | + */ |
| 6 | + |
| 7 | +/** |
| 8 | + * Settings the Node.js SDK follows, with the kind of value each one takes: |
| 9 | + * "switch" true or false; "count" a whole number of at least 1; "zeroOrMore" a whole number of at least 0; |
| 10 | + * "fraction" a number above 0 and at most 1 |
| 11 | + */ |
| 12 | +var SUPPORTED = { |
| 13 | + tracking: "switch", |
| 14 | + networking: "switch", |
| 15 | + st: "switch", |
| 16 | + vt: "switch", |
| 17 | + cet: "switch", |
| 18 | + crt: "switch", |
| 19 | + lt: "switch", |
| 20 | + cr: "switch", |
| 21 | + bom: "switch", |
| 22 | + rqs: "count", |
| 23 | + eqs: "count", |
| 24 | + sui: "count", |
| 25 | + lkl: "count", |
| 26 | + lvs: "count", |
| 27 | + lsv: "count", |
| 28 | + lbc: "count", |
| 29 | + ltlpt: "count", |
| 30 | + ltl: "count", |
| 31 | + scui: "count", |
| 32 | + bom_at: "count", |
| 33 | + bom_ra: "count", |
| 34 | + bom_d: "count", |
| 35 | + dort: "zeroOrMore", |
| 36 | + bom_rqp: "fraction", |
| 37 | +}; |
| 38 | + |
| 39 | +/** |
| 40 | + * Values used when neither the server nor the developer gives one |
| 41 | + */ |
| 42 | +var DEFAULTS = { |
| 43 | + tracking: true, |
| 44 | + networking: true, |
| 45 | + st: true, |
| 46 | + vt: true, |
| 47 | + cet: true, |
| 48 | + crt: true, |
| 49 | + lt: true, |
| 50 | + cr: false, |
| 51 | + bom: true, |
| 52 | + scui: 4, |
| 53 | + dort: 0, |
| 54 | + bom_at: 10, |
| 55 | + bom_rqp: 0.5, |
| 56 | + bom_ra: 24, |
| 57 | + bom_d: 60, |
| 58 | +}; |
| 59 | + |
| 60 | +/** |
| 61 | + * Longest settings refresh interval in hours. A Node.js timer cannot wait longer than about 24.8 days, and a |
| 62 | + * longer value would make it fire at once, over and over. |
| 63 | + */ |
| 64 | +var MAX_REFRESH_HOURS = 596; |
| 65 | + |
| 66 | +/** |
| 67 | + * Tells whether a value is a plain object (not null, not an array) |
| 68 | + * @param {*} value - value to check |
| 69 | + * @returns {Boolean} true for a plain object |
| 70 | + */ |
| 71 | +function isPlainObject(value) { |
| 72 | + return Object.prototype.toString.call(value) === "[object Object]"; |
| 73 | +} |
| 74 | + |
| 75 | +/** |
| 76 | + * Tells whether a value fits the kind a setting takes |
| 77 | + * @param {String} kind - "switch", "count", "zeroOrMore" or "fraction" |
| 78 | + * @param {*} value - value from the server or the developer |
| 79 | + * @returns {Boolean} true if the value can be used |
| 80 | + */ |
| 81 | +function isValidValue(kind, value) { |
| 82 | + if (kind === "switch") { |
| 83 | + return typeof value === "boolean"; |
| 84 | + } |
| 85 | + if (typeof value !== "number" || !Number.isFinite(value)) { |
| 86 | + return false; |
| 87 | + } |
| 88 | + if (kind === "count") { |
| 89 | + return Math.floor(value) === value && value >= 1; |
| 90 | + } |
| 91 | + if (kind === "zeroOrMore") { |
| 92 | + return Math.floor(value) === value && value >= 0; |
| 93 | + } |
| 94 | + return value > 0 && value <= 1; |
| 95 | +} |
| 96 | + |
| 97 | +/** |
| 98 | + * Keeps only the supported settings with a usable value. Other keys (for example the dashboard's |
| 99 | + * "bom_preset", or settings for other platforms) are left out, and an unusable value is left out with a |
| 100 | + * warning, so the developer value or the default stays in use for it. |
| 101 | + * @param {Object} config - the "c" object of a settings answer |
| 102 | + * @param {Function} warn - called with a message for each left out value |
| 103 | + * @returns {Object} the usable settings |
| 104 | + */ |
| 105 | +function normalize(config, warn) { |
| 106 | + var result = {}; |
| 107 | + Object.keys(config).forEach((key) => { |
| 108 | + if (!Object.prototype.hasOwnProperty.call(SUPPORTED, key)) { |
| 109 | + return; |
| 110 | + } |
| 111 | + if (isValidValue(SUPPORTED[key], config[key])) { |
| 112 | + result[key] = key === "scui" ? Math.min(config[key], MAX_REFRESH_HOURS) : config[key]; |
| 113 | + } |
| 114 | + else if (typeof warn === "function") { |
| 115 | + warn(`The behavior setting [${key}] has an unusable value [${JSON.stringify(config[key])}], it is ignored.`); |
| 116 | + } |
| 117 | + }); |
| 118 | + return result; |
| 119 | +} |
| 120 | + |
| 121 | +/** |
| 122 | + * Reads the server's answer to a settings request. The answer must be a JSON object with a "c" object; |
| 123 | + * other top-level keys (v, t, lg, ct and future ones) are ignored, so both the v1 and the v2 format work. |
| 124 | + * @param {String} responseText - the answer body |
| 125 | + * @param {Function} warn - called with a message for each problem |
| 126 | + * @returns {Object|null} the usable settings, or null when the answer cannot be used |
| 127 | + */ |
| 128 | +function parseAnswer(responseText, warn) { |
| 129 | + var answer; |
| 130 | + try { |
| 131 | + answer = JSON.parse(responseText); |
| 132 | + } |
| 133 | + catch (ex) { |
| 134 | + return null; |
| 135 | + } |
| 136 | + if (!isPlainObject(answer) || !isPlainObject(answer.c)) { |
| 137 | + return null; |
| 138 | + } |
| 139 | + return normalize(answer.c, warn); |
| 140 | +} |
| 141 | + |
| 142 | +/** |
| 143 | + * Reads the settings a developer provides with the "behavior_settings" init option, in the same format the |
| 144 | + * server sends: an object with a "c" object |
| 145 | + * @param {*} provided - the init option value |
| 146 | + * @param {Function} warn - called with a message for each problem |
| 147 | + * @returns {Object|null} the usable settings, or null when none are provided or the format is wrong |
| 148 | + */ |
| 149 | +function parseProvided(provided, warn) { |
| 150 | + if (typeof provided === "undefined" || provided === null) { |
| 151 | + return null; |
| 152 | + } |
| 153 | + if (!isPlainObject(provided) || !isPlainObject(provided.c)) { |
| 154 | + if (typeof warn === "function") { |
| 155 | + warn("behavior_settings must be an object with a \"c\" object, as SDK Manager exports it. It is ignored."); |
| 156 | + } |
| 157 | + return null; |
| 158 | + } |
| 159 | + return normalize(provided.c, warn); |
| 160 | +} |
| 161 | + |
| 162 | +/** |
| 163 | + * Reads settings stored by an earlier run |
| 164 | + * @param {*} stored - the stored value |
| 165 | + * @returns {Object|null} the usable settings, or null when nothing usable is stored |
| 166 | + */ |
| 167 | +function parseStored(stored) { |
| 168 | + if (!isPlainObject(stored)) { |
| 169 | + return null; |
| 170 | + } |
| 171 | + return normalize(stored); |
| 172 | +} |
| 173 | + |
| 174 | +/** |
| 175 | + * Decides the value of a setting: the server's (or the stored or provided) value when there is one, else |
| 176 | + * the developer's value, else the default |
| 177 | + * @param {Object} settings - settings in use |
| 178 | + * @param {String} key - setting key |
| 179 | + * @param {*=} developerValue - value set with an init option, undefined when none |
| 180 | + * @returns {*} the value to use |
| 181 | + */ |
| 182 | +function valueOf(settings, key, developerValue) { |
| 183 | + if (settings && Object.prototype.hasOwnProperty.call(settings, key)) { |
| 184 | + return settings[key]; |
| 185 | + } |
| 186 | + if (typeof developerValue !== "undefined") { |
| 187 | + return developerValue; |
| 188 | + } |
| 189 | + return DEFAULTS[key]; |
| 190 | +} |
| 191 | + |
| 192 | +/** |
| 193 | + * Decides whether sending must pause after a successful request, following the backoff settings: the |
| 194 | + * request took at least bom_at seconds, the queue is at most bom_rqp of its size, and the request was |
| 195 | + * recorded at most bom_ra hours ago |
| 196 | + * @param {Object} input - measurements and settings |
| 197 | + * @param {Number} input.durationSeconds - how long the request took |
| 198 | + * @param {Number} input.queueLength - requests left in the queue |
| 199 | + * @param {Number} input.queueSize - maximum queue size |
| 200 | + * @param {Number} input.ageHours - hours since the request was recorded |
| 201 | + * @param {Number} input.bom_at - response time threshold in seconds |
| 202 | + * @param {Number} input.bom_rqp - queue fill threshold as a fraction of the queue size |
| 203 | + * @param {Number} input.bom_ra - request age threshold in hours |
| 204 | + * @returns {Boolean} true when sending must pause |
| 205 | + */ |
| 206 | +function shouldBackOff(input) { |
| 207 | + return input.durationSeconds >= input.bom_at |
| 208 | + && input.queueLength <= input.queueSize * input.bom_rqp |
| 209 | + && input.ageHours <= input.bom_ra; |
| 210 | +} |
| 211 | + |
| 212 | +module.exports = { |
| 213 | + SUPPORTED, |
| 214 | + DEFAULTS, |
| 215 | + MAX_REFRESH_HOURS, |
| 216 | + normalize, |
| 217 | + parseAnswer, |
| 218 | + parseProvided, |
| 219 | + parseStored, |
| 220 | + valueOf, |
| 221 | + shouldBackOff, |
| 222 | +}; |
0 commit comments