diff --git a/CHANGES.txt b/CHANGES.txt index 1dcf3090b..79303ed5b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,9 @@ +10.19.0 (May 24, 2022) + - Added `scheduler.telemetryRefreshRate` property to SDK configuration, and deprecated `scheduler.metricsRefreshRate` property. + - Updated SDK telemetry storage, metrics and updater to be more effective and send less often. + - Bugfixing - Updated default values for `scheduler.impressionsRefreshRate` config parameter: 300s for OPTIMIZED impression mode and 60s for DEBUG impression mode, + in both Browser and NodeJS (previously it was 60s and 300s in Browser and NodeJS respectively, equal for DEBUG and OPTIMIZED impression modes). + 10.18.2 (May 6, 2022) - Bugfixing - Removed "ip" dependency to use an inline copy of it that fixes an error when using it in Node v18 (https://github.com/indutny/node-ip/issues/113). diff --git a/package-lock.json b/package-lock.json index ec5ba958f..7891466df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio", - "version": "10.18.2", + "version": "10.19.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -437,9 +437,9 @@ "dev": true }, "@splitsoftware/splitio-commons": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.3.1.tgz", - "integrity": "sha512-otHrBCNJ2BP2tby31juRFfBETYzBVJZVecg1d27o9v5IKsv9uhzF9Q8B35GPpbndFht/596O2OlxKH5HdbjBwQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.4.0.tgz", + "integrity": "sha512-i/gXbtzUMKotva66mkPpa8g49v+gR4AbA/ZTKBGFi4CN+blwFO/10YQQWro8Gm9YJIB1mTXdplem6XkeBUzISA==", "requires": { "tslib": "^2.3.1" } @@ -2818,9 +2818,9 @@ "dev": true }, "eventsource": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.0.7.tgz", - "integrity": "sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.1.tgz", + "integrity": "sha512-qV5ZC0h7jYIAOhArFJgSfdyz6rALJyb270714o7ZtNnw2WSJ+eexhKtE0O8LYPRsHZHf2osHKZBxGPvm3kPkCA==", "optional": true, "requires": { "original": "^1.0.0" @@ -4936,9 +4936,9 @@ "dev": true }, "querystringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz", - "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", "optional": true }, "randombytes": { @@ -6154,9 +6154,9 @@ } }, "tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" }, "tty-browserify": { "version": "0.0.1", diff --git a/package.json b/package.json index 122475aef..8e16d9430 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio", - "version": "10.18.2", + "version": "10.19.0", "description": "Split SDK", "files": [ "README.md", @@ -32,7 +32,7 @@ "node": ">=6" }, "dependencies": { - "@splitsoftware/splitio-commons": "1.3.1", + "@splitsoftware/splitio-commons": "1.4.0", "@types/google.analytics": "0.0.40", "@types/ioredis": "^4.28.0", "ioredis": "^4.28.0", diff --git a/src/__tests__/browser.spec.js b/src/__tests__/browser.spec.js index c1993c405..618a862b5 100644 --- a/src/__tests__/browser.spec.js +++ b/src/__tests__/browser.spec.js @@ -4,7 +4,7 @@ import { url } from './testUtils'; import evaluationsSuite from './browserSuites/evaluations.spec'; import impressionsSuite from './browserSuites/impressions.spec'; import impressionsSuiteDebug from './browserSuites/impressions.debug.spec'; -// import metricsSuite from './browserSuites/metrics.spec'; +import telemetrySuite from './browserSuites/telemetry.spec'; import impressionsListenerSuite from './browserSuites/impressions-listener.spec'; import readinessSuite from './browserSuites/readiness.spec'; import readyFromCache from './browserSuites/ready-from-cache.spec'; @@ -45,7 +45,6 @@ const configInMemory = { scheduler: { featuresRefreshRate: 1, segmentsRefreshRate: 1, - metricsRefreshRate: 3000, // for now I don't want to publish metrics during E2E run. impressionsRefreshRate: 3000 // for now I don't want to publish impressions during E2E run. }, streamingEnabled: false @@ -62,7 +61,6 @@ const configInMemoryWithBucketingKey = { scheduler: { featuresRefreshRate: 1, segmentsRefreshRate: 1, - metricsRefreshRate: 3000, // for now I don't want to publish metrics during E2E run. impressionsRefreshRate: 3000 // for now I don't want to publish impressions during E2E run. }, streamingEnabled: false @@ -76,7 +74,6 @@ const configInLocalStorage = { scheduler: { featuresRefreshRate: 1, segmentsRefreshRate: 1, - metricsRefreshRate: 3000, // for now I don't want to publish metrics during E2E run. impressionsRefreshRate: 3000 // for now I don't want to publish impressions during E2E run. }, storage: { @@ -108,9 +105,8 @@ tape('## E2E CI Tests ##', function(assert) { assert.test('E2E / Impressions Debug Mode', impressionsSuiteDebug.bind(null, fetchMock)); /* Check impression listener */ assert.test('E2E / Impression listener', impressionsListenerSuite); - /* Check metrics */ - // @TODO uncomment when telemetry is implemented - // assert.test('E2E / Metrics', metricsSuite.bind(null, fetchMock)); + /* Check telemetry */ + assert.test('E2E / Telemetry', telemetrySuite.bind(null, fetchMock)); /* Check events */ assert.test('E2E / Events', withoutBindingTT.bind(null, fetchMock)); assert.test('E2E / Events with TT binded', bindingTT.bind(null, fetchMock)); diff --git a/src/__tests__/browserSuites/events.spec.js b/src/__tests__/browserSuites/events.spec.js index 1d18a6e66..396f01d35 100644 --- a/src/__tests__/browserSuites/events.spec.js +++ b/src/__tests__/browserSuites/events.spec.js @@ -17,7 +17,6 @@ const baseSettings = { scheduler: { featuresRefreshRate: 1, segmentsRefreshRate: 1, - metricsRefreshRate: 3000, impressionsRefreshRate: 3000, eventsPushRate: 3000 }, diff --git a/src/__tests__/browserSuites/ignore-ip-addresses-setting.spec.js b/src/__tests__/browserSuites/ignore-ip-addresses-setting.spec.js index 80e1f27cc..05383e38b 100644 --- a/src/__tests__/browserSuites/ignore-ip-addresses-setting.spec.js +++ b/src/__tests__/browserSuites/ignore-ip-addresses-setting.spec.js @@ -11,7 +11,6 @@ const HEADER_SPLITSDKMACHINENAME = 'SplitSDKMachineName'; // Refresh rates are set to 1 second to finish the test quickly. Otherwise, it would finish in 1 minute (60 seconds is the default value) const baseConfig = { scheduler: { - metricsRefreshRate: 1, impressionsRefreshRate: 1, eventsPushRate: 1 }, @@ -71,9 +70,6 @@ const configSamples = [ const postEndpoints = [ '/events/bulk', '/testImpressions/bulk', - // @TODO uncomment when telemetry is implemented - // '/metrics/times', - // '/metrics/counters' ]; export default function (fetchMock, assert) { diff --git a/src/__tests__/browserSuites/impressions-listener.spec.js b/src/__tests__/browserSuites/impressions-listener.spec.js index 79339b54c..5ca928804 100644 --- a/src/__tests__/browserSuites/impressions-listener.spec.js +++ b/src/__tests__/browserSuites/impressions-listener.spec.js @@ -21,7 +21,6 @@ const config = { scheduler: { featuresRefreshRate: 1, segmentsRefreshRate: 1, - metricsRefreshRate: 3000, impressionsRefreshRate: 3000 }, startup: { diff --git a/src/__tests__/browserSuites/impressions.debug.spec.js b/src/__tests__/browserSuites/impressions.debug.spec.js index f8ceb5e2f..a4b182dce 100644 --- a/src/__tests__/browserSuites/impressions.debug.spec.js +++ b/src/__tests__/browserSuites/impressions.debug.spec.js @@ -33,7 +33,6 @@ export default function (fetchMock, assert) { scheduler: { featuresRefreshRate: 0.5, segmentsRefreshRate: 0.5, - metricsRefreshRate: 3000, impressionsRefreshRate: 3000, impressionsQueueSize: 3 // flush impressions when 3 are queued }, diff --git a/src/__tests__/browserSuites/impressions.spec.js b/src/__tests__/browserSuites/impressions.spec.js index c20506d30..e5ebb5092 100644 --- a/src/__tests__/browserSuites/impressions.spec.js +++ b/src/__tests__/browserSuites/impressions.spec.js @@ -36,7 +36,6 @@ export default function (fetchMock, assert) { scheduler: { featuresRefreshRate: 0.5, segmentsRefreshRate: 0.5, - metricsRefreshRate: 3000, impressionsRefreshRate: 0.5 }, startup: { diff --git a/src/__tests__/browserSuites/metrics.spec.js b/src/__tests__/browserSuites/metrics.spec.js deleted file mode 100644 index 8841a9515..000000000 --- a/src/__tests__/browserSuites/metrics.spec.js +++ /dev/null @@ -1,155 +0,0 @@ -import { SplitFactory } from '../../'; -import splitChangesMock1 from '../mocks/splitchanges.since.-1.json'; -import splitChangesMock2 from '../mocks/splitchanges.since.1457552620999.json'; - -const baseUrls = { - sdk: 'https://sdk.baseurl/metricsSuite', - events: 'https://events.baseurl/metricsSuite' -}; - -const config = { - core: { - authorizationKey: '', - key: 'metrics-browser-tests-key' - }, - scheduler: { - featuresRefreshRate: 99999, - segmentsRefreshRate: 99999, - metricsRefreshRate: 3, - impressionsRefreshRate: 99999 - }, - urls: baseUrls, - startup: { - eventsFirstPushWindow: 3000 - }, - streamingEnabled: false -}; - -export default async function metricsBrowserSuite(fetchMock, assert) { - fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=-1', 500); - fetchMock.get(baseUrls.sdk + '/splitChanges?since=-1', { status: 200, body: splitChangesMock1 }); - fetchMock.getOnce(baseUrls.sdk + '/mySegments/metrics-browser-tests-key', 500); - fetchMock.get(baseUrls.sdk + '/mySegments/metrics-browser-tests-key', { status: 200, body: { 'mySegments': [] } }); - // Should not execute but adding just in case. - fetchMock.get(baseUrls.sdk + '/splitChanges?since=1457552620999', { status: 200, body: splitChangesMock2 }); - // We need to handle all requests properly - fetchMock.postOnce(baseUrls.events + '/testImpressions/bulk', 200); - fetchMock.postOnce(baseUrls.events + '/testImpressions/count', 200); - - const splitio = SplitFactory(config); - const client = splitio.client(); - - const finish = (function* () { - yield; - client.destroy(); - assert.end(); - })(); - - fetchMock.postOnce(baseUrls.events + '/metrics/times', (url, opts) => { - const data = JSON.parse(opts.body); - - assert.equal(data.length, 7, 'We performed 4 correct evaluation requests (one per method) plus ready, splits and segments, so we should have 7 latency metrics.'); - - const latencyMetricsRecorded = data.filter(metric => { - // At least one latency registed per metric - return metric.latencies.some(count => count > 0); - }); - - // If we have 7 items, it is because each one had at least ONE entry. - assert.equal(latencyMetricsRecorded.length, 7, 'Each metric has at least one enty, matching the calls.'); - - const getLatencyCount = metricName => { - const latencyMetric = data.find(metric => metric.name === metricName); - - if (!latencyMetric && !Array.isArray(latencyMetric.latencies)) return 0; - - return latencyMetric.latencies.reduce((accum, entry) => accum + entry, 0); - }; - - // Validate both names and values. - assert.equal(getLatencyCount('splitChangeFetcher.time'), 2, 'Two latency metrics for splitChanges'); // 1 err 1 ok - assert.equal(getLatencyCount('mySegmentsFetcher.time'), 2, 'One latency metric for mySegmentsFetcher'); // 1 err 1 ok - assert.equal(getLatencyCount('sdk.ready'), 1, 'One latency metric for ready'); - assert.equal(getLatencyCount('sdk.getTreatment'), 1, 'One latency metric for getTreatment'); - assert.equal(getLatencyCount('sdk.getTreatments'), 1, 'One latency metric for getTreatments'); - assert.equal(getLatencyCount('sdk.getTreatmentWithConfig'), 1, 'One latency metric for getTreatmentWithConfig'); - assert.equal(getLatencyCount('sdk.getTreatmentsWithConfig'), 1, 'One latency metric for getTreatmentsWithConfig'); - - finish.next(); - - return 200; - }); - - fetchMock.postOnce(baseUrls.events + '/metrics/counters', (url, opts) => { - const data = JSON.parse(opts.body); - - assert.equal(data.length, 4, 'Based on the mock setup, we should have four items.'); - - const countMetricsRecorded = data.reduce((accum, metric) => accum + metric.delta, 0); - - const getRecodsCount = metricName => { - const countMetric = data.find(metric => metric.name === metricName); - - if (!countMetric) return 0; - - return countMetric.delta; - }; - - // 4 items: - // For splitChanges, 1 exception and 1 200. - // For mySegments 1 exception and 1 200. - assert.equal(countMetricsRecorded, 4, 'Each metric has one entry, same as the amount of calls.'); - // break down - assert.equal(getRecodsCount('splitChangeFetcher.exception'), 1, 'The metric names and delta should correspond to the SDK behaviour.'); - assert.equal(getRecodsCount('mySegmentsFetcher.exception'), 1, 'The metric names and delta should correspond to the SDK behaviour.'); - assert.equal(getRecodsCount('splitChangeFetcher.status.200'), 1, 'The metric names and delta should correspond to the SDK behaviour.'); - assert.equal(getRecodsCount('mySegmentsFetcher.status.200'), 1, 'The metric names and delta should correspond to the SDK behaviour.'); - - finish.next(); - - return 200; - }); - - await client.ready(); - - // treatments and results are only validated so we know for sure when the function was actually running to compare the metrics. - assert.equal(client.getTreatment('always_on'), 'on', 'Evaluation was correct.'); - assert.equal(client.getTreatment('always_on', () => { }), 'control', 'We should return control with invalid input.'); - - assert.deepEqual(client.getTreatmentWithConfig('split_with_config'), { - treatment: 'o.n', - config: '{"color":"brown","dimensions":{"height":12,"width":14},"text":{"inner":"click me"}}' - }, 'Evaluation with config was correct.'); - assert.deepEqual(client.getTreatmentWithConfig('split_with_config', () => { }), { - treatment: 'control', - config: null - }, 'Evaluation with config returned control state for invalid input.'); - - assert.deepEqual(client.getTreatments(['always_on', 'always_off']), { always_on: 'on', always_off: 'off' }, 'Evaluations were correct.'); - assert.deepEqual(client.getTreatments(['always_on', 'always_off', null], () => { }), { always_on: 'control', always_off: 'control' }, 'We should return map of controls with invalid input.'); - - assert.deepEqual(client.getTreatmentsWithConfig(['split_with_config', 'always_on', null]), - { - split_with_config: { - treatment: 'o.n', - config: '{"color":"brown","dimensions":{"height":12,"width":14},"text":{"inner":"click me"}}' - }, - always_on: { - treatment: 'on', - config: null - } - } - , 'Evaluations with config were correct.'); - assert.deepEqual(client.getTreatmentsWithConfig(['split_with_config', 'always_on', null], () => { }), - { - split_with_config: { - treatment: 'control', - config: null - }, - always_on: { - treatment: 'control', - config: null - } - }, - 'Evaluations with config returned control states for invalid input.'); -} diff --git a/src/__tests__/browserSuites/push-fallbacking.spec.js b/src/__tests__/browserSuites/push-fallbacking.spec.js index fafb3725e..878a3f5ea 100644 --- a/src/__tests__/browserSuites/push-fallbacking.spec.js +++ b/src/__tests__/browserSuites/push-fallbacking.spec.js @@ -51,7 +51,6 @@ const config = { scheduler: { featuresRefreshRate: 0.2, segmentsRefreshRate: 0.25, - metricsRefreshRate: 3000, impressionsRefreshRate: 3000 }, urls: baseUrls, diff --git a/src/__tests__/browserSuites/push-initialization-nopush.spec.js b/src/__tests__/browserSuites/push-initialization-nopush.spec.js index d337501f8..6e0e0a4f7 100644 --- a/src/__tests__/browserSuites/push-initialization-nopush.spec.js +++ b/src/__tests__/browserSuites/push-initialization-nopush.spec.js @@ -24,7 +24,6 @@ const config = { scheduler: { featuresRefreshRate: 0.1, segmentsRefreshRate: 0.1, - metricsRefreshRate: 3000, impressionsRefreshRate: 3000, pushRetryBackoffBase: 0.01 // small value to assert rapidly that push is not retried }, diff --git a/src/__tests__/browserSuites/push-initialization-retries.spec.js b/src/__tests__/browserSuites/push-initialization-retries.spec.js index 7fd440c2d..6585076d8 100644 --- a/src/__tests__/browserSuites/push-initialization-retries.spec.js +++ b/src/__tests__/browserSuites/push-initialization-retries.spec.js @@ -26,7 +26,6 @@ const config = { scheduler: { featuresRefreshRate: 0.2, segmentsRefreshRate: 0.2, - metricsRefreshRate: 3000, impressionsRefreshRate: 3000, pushRetryBackoffBase: 0.1 }, diff --git a/src/__tests__/browserSuites/readiness.spec.js b/src/__tests__/browserSuites/readiness.spec.js index 5956af465..c6897fafd 100644 --- a/src/__tests__/browserSuites/readiness.spec.js +++ b/src/__tests__/browserSuites/readiness.spec.js @@ -22,7 +22,6 @@ const baseConfig = { scheduler: { featuresRefreshRate: 3000, segmentsRefreshRate: 3000, - metricsRefreshRate: 3000, impressionsRefreshRate: 3000 }, startup: { @@ -164,7 +163,6 @@ export default function (fetchMock, assert) { scheduler: { featuresRefreshRate: 3.1, segmentsRefreshRate: 1, - metricsRefreshRate: 3000, impressionsRefreshRate: 3000 }, urls: testUrls @@ -245,7 +243,6 @@ export default function (fetchMock, assert) { scheduler: { featuresRefreshRate: 3.1, segmentsRefreshRate: 1, - metricsRefreshRate: 3000, impressionsRefreshRate: 3000 }, storage: { @@ -330,7 +327,6 @@ export default function (fetchMock, assert) { scheduler: { featuresRefreshRate: 3.1, segmentsRefreshRate: 1, - metricsRefreshRate: 3000, impressionsRefreshRate: 3000 }, urls: testUrls @@ -411,7 +407,6 @@ export default function (fetchMock, assert) { scheduler: { featuresRefreshRate: 3.1, segmentsRefreshRate: 1, - metricsRefreshRate: 3000, impressionsRefreshRate: 3000 }, storage: { @@ -481,7 +476,6 @@ export default function (fetchMock, assert) { scheduler: { featuresRefreshRate: 3.1, segmentsRefreshRate: 1, - metricsRefreshRate: 3000, impressionsRefreshRate: 3000 }, storage: { @@ -529,7 +523,6 @@ export default function (fetchMock, assert) { scheduler: { featuresRefreshRate: 3.1, segmentsRefreshRate: 1, - metricsRefreshRate: 3000, impressionsRefreshRate: 3000 }, storage: { @@ -581,7 +574,6 @@ export default function (fetchMock, assert) { scheduler: { featuresRefreshRate: 3.1, segmentsRefreshRate: 1, - metricsRefreshRate: 3000, impressionsRefreshRate: 3000 }, storage: { @@ -629,7 +621,6 @@ export default function (fetchMock, assert) { scheduler: { featuresRefreshRate: 3.1, segmentsRefreshRate: 1, - metricsRefreshRate: 3000, impressionsRefreshRate: 3000 }, storage: { @@ -680,7 +671,6 @@ export default function (fetchMock, assert) { scheduler: { featuresRefreshRate: 3.1, segmentsRefreshRate: 1, - metricsRefreshRate: 3000, impressionsRefreshRate: 3000 }, storage: { diff --git a/src/__tests__/browserSuites/ready-from-cache.spec.js b/src/__tests__/browserSuites/ready-from-cache.spec.js index fe52597a8..45a56012e 100644 --- a/src/__tests__/browserSuites/ready-from-cache.spec.js +++ b/src/__tests__/browserSuites/ready-from-cache.spec.js @@ -72,7 +72,6 @@ const baseConfig = { scheduler: { featuresRefreshRate: 3000, segmentsRefreshRate: 3000, - metricsRefreshRate: 3000, impressionsRefreshRate: 3000 }, startup: { diff --git a/src/__tests__/browserSuites/telemetry.spec.js b/src/__tests__/browserSuites/telemetry.spec.js new file mode 100644 index 000000000..e6ebbad22 --- /dev/null +++ b/src/__tests__/browserSuites/telemetry.spec.js @@ -0,0 +1,163 @@ +import { SplitFactory } from '../../'; +import splitChangesMock1 from '../mocks/splitchanges.since.-1.json'; + +const baseUrls = { + sdk: 'https://sdk.baseurl/telemetrySuite', + events: 'https://events.baseurl/telemetrySuite', + telemetry: 'https://telemetry.baseurl/telemetrySuite', +}; + +const config = { + core: { + authorizationKey: '', + key: 'user-key' + }, + scheduler: { + featuresRefreshRate: 99999, + telemetryRefreshRate: 60 + }, + urls: baseUrls, + streamingEnabled: false +}; + +export default async function telemetryBrowserSuite(fetchMock, assert) { + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=-1', 500); + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=-1', { status: 200, body: splitChangesMock1 }); + fetchMock.getOnce(baseUrls.sdk + '/mySegments/user-key', 500); + fetchMock.getOnce(baseUrls.sdk + '/mySegments/user-key', { status: 200, body: { 'mySegments': [ 'one_segment'] } }); + + // We need to handle all requests properly + fetchMock.postOnce(baseUrls.events + '/testImpressions/bulk', 200); + fetchMock.postOnce(baseUrls.events + '/testImpressions/count', 200); + fetchMock.postOnce(baseUrls.events + '/events/bulk', 200); + + // Overwrite Math.random to instantiate factory with telemetry + const originalMathRandom = Math.random; Math.random = () => 0.001; + const splitio = SplitFactory(config, ({ settings }) => { + assert.equal(settings.scheduler.telemetryRefreshRate, 60000); + settings.scheduler.telemetryRefreshRate = 1000; // set below minimum to validate matrics/usage requests + }); + Math.random = originalMathRandom; // restore + + const client = splitio.client(); + + const finish = (function* () { + yield; + client.destroy(); + assert.end(); + })(); + + let lastSync; + + // 1st metrics/usage call due to telemetryRefreshRate set in 1 second + fetchMock.postOnce(baseUrls.telemetry + '/v1/metrics/usage', (url, opts) => { + const data = JSON.parse(opts.body); + + // Validate last successful sync + assert.deepEqual(Object.keys(data.lS), ['sp', 'ms', 'te'], 'Successful splitChanges, mySegments and metrics/config requests'); + lastSync = data.lS; delete data.lS; + + // Validate http and method latencies + const getLatencyCount = buckets => buckets ? buckets.reduce((accum, entry) => accum + entry, 0) : 0; + assert.equal(getLatencyCount(data.hL.sp), 2, 'Two latency metrics for splitChanges GET request'); + assert.equal(getLatencyCount(data.hL.ms), 2, 'Two latency metrics for mySegments GET request'); + assert.equal(getLatencyCount(data.hL.te), 1, 'One latency metric for telemetry config POST request'); + assert.equal(getLatencyCount(data.mL.t), 2, 'Two latency metrics for getTreatment (one not ready usage'); + assert.equal(getLatencyCount(data.mL.ts), 1, 'One latency metric for getTreatments'); + assert.equal(getLatencyCount(data.mL.tc), 1, 'One latency metric for getTreatmentWithConfig'); + assert.equal(getLatencyCount(data.mL.tcs), 1, 'One latency metric for getTreatmentsWithConfig'); + assert.equal(getLatencyCount(data.mL.tr), 1, 'One latency metric for track'); + delete data.hL; delete data.mL; + + assert.deepEqual(data, { + mE: {}, hE: { sp: { 500: 1 }, ms: { 500: 1 } }, tR: 0, aR: 0, iQ: 4, iDe: 3, iDr: 0, spC: 31, seC: 1, skC: 1, eQ: 1, eD: 0, sE: [], t: [] + }, 'metrics/usage JSON payload should be the expected'); + + finish.next(); + return 200; + }); + + // 2nd metrics/usage call due to destroy + fetchMock.postOnce(baseUrls.telemetry + '/v1/metrics/usage', (url, opts) => { + const data = JSON.parse(opts.body); + + assert.deepEqual(data.lS, lastSync, 'last successful sync hasn\'t change'); + delete data.lS; + + assert.true(data.sL > 0, 'sessionLengthMs must be defined'); + delete data.sL; + + assert.deepEqual(data, { + mL: {}, mE: {}, hE: {}, hL: {}, // errors and latencies were popped + tR: 0, aR: 0, iQ: 4, iDe: 3, iDr: 0, spC: 31, seC: 1, skC: 1, eQ: 1, eD: 0, sE: [], t: [] + }, '2nd metrics/usage JSON payload should be the expected'); + return 200; + }); + + fetchMock.postOnce(baseUrls.telemetry + '/v1/metrics/config', (url, opts) => { + const data = JSON.parse(opts.body); + + assert.true(data.tR > 0, 'timeUntilReady is larger than 0'); + delete data.tR; // delete to validate other properties + + assert.deepEqual(data, { + oM: 0, st: 'memory', aF: 1, rF: 0, sE: false, + rR: { sp: 99999, ms: 60, im: 300, ev: 60, te: 1 } /* override featuresRefreshRate */, + uO: { s: true, e: true, a: false, st: false, t: true } /* override sdk, events and telemetry URLs */, + iQ: 30000, eQ: 500, iM: 0, iL: false, hP: false, nR: 1 /* 1 non ready usage */, t: [], i: [], uC: 2 /* Default GRANTED */ + }, 'metrics/config JSON payload should be the expected'); + + finish.next(); + + return 200; + }); + + assert.equal(client.getTreatment('always_on'), 'control', 'Non ready usage.'); + + await client.ready(); + + // treatments and results are only validated so we know for sure when the function was actually running to compare the metrics. + assert.equal(client.getTreatment('always_on'), 'on', 'Evaluation was correct.'); + assert.equal(client.getTreatment('always_on', () => { }), 'control', 'We should return control with invalid input.'); + + assert.deepEqual(client.getTreatmentWithConfig('split_with_config'), { + treatment: 'o.n', + config: '{"color":"brown","dimensions":{"height":12,"width":14},"text":{"inner":"click me"}}' + }, 'Evaluation with config was correct.'); + assert.deepEqual(client.getTreatmentWithConfig('split_with_config', () => { }), { + treatment: 'control', + config: null + }, 'Evaluation with config returned control state for invalid input.'); + + assert.deepEqual(client.getTreatments(['always_on', 'always_off']), { always_on: 'on', always_off: 'off' }, 'Evaluations were correct.'); + assert.deepEqual(client.getTreatments(['always_on', 'always_off', null], () => { }), { always_on: 'control', always_off: 'control' }, 'We should return map of controls with invalid input.'); + + assert.deepEqual(client.getTreatmentsWithConfig(['split_with_config', 'always_on', null]), + { + split_with_config: { + treatment: 'o.n', + config: '{"color":"brown","dimensions":{"height":12,"width":14},"text":{"inner":"click me"}}' + }, + always_on: { + treatment: 'on', + config: null + } + } + , 'Evaluations with config were correct.'); + assert.deepEqual(client.getTreatmentsWithConfig(['split_with_config', 'always_on', null], () => { }), + { + split_with_config: { + treatment: 'control', + config: null + }, + always_on: { + treatment: 'control', + config: null + } + }, + 'Evaluations with config returned control states for invalid input.'); + + assert.equal(client.track('someTT', 'someEvent'), true, 'Event was queued'); + assert.equal(client.track('someTT', null), false, 'Invalid input.'); + +} diff --git a/src/__tests__/destroy/node.spec.js b/src/__tests__/destroy/node.spec.js index e24b3c8ef..0acb88e05 100644 --- a/src/__tests__/destroy/node.spec.js +++ b/src/__tests__/destroy/node.spec.js @@ -19,6 +19,7 @@ import impressionsMock from './impressions.json'; fetchMock.get(url(settings, '/splitChanges?since=-1'), { status: 200, body: splitChangesMock1 }); fetchMock.get(url(settings, '/splitChanges?since=-1500492097547'), { status: 200, body: splitChangesMock2 }); +fetchMock.postOnce(url(settings, '/v1/metrics/config'), 200); tape('SDK destroy for NodeJS', async function (assert) { const config = { @@ -74,6 +75,15 @@ tape('SDK destroy for NodeJS', async function (assert) { return 200; }); + // Assert we are sending telemetry stats while doing the destroy + fetchMock.postOnce(url(settings, '/v1/metrics/usage'), (url, opts) => { + const payload = JSON.parse(opts.body); + + assert.true(payload.sL > 0, 'Should flush telemetry stats with session length on destroy.'); + + return 200; + }); + await client.ready(); assert.equal(client.getTreatment('ut1', 'Single_Test'), 'on'); diff --git a/src/__tests__/errorCatching/browser.spec.js b/src/__tests__/errorCatching/browser.spec.js index f5e3e2e7c..af5a0cabe 100644 --- a/src/__tests__/errorCatching/browser.spec.js +++ b/src/__tests__/errorCatching/browser.spec.js @@ -48,7 +48,7 @@ tape('Error catching on callbacks - Browsers', assert => { scheduler: { featuresRefreshRate: 1.5, segmentsRefreshRate: 100000, - metricsRefreshRate: 100000, + telemetryRefreshRate: 100000, impressionsRefreshRate: 100000, eventsPushRate: 100000 }, diff --git a/src/__tests__/errorCatching/node.spec.js b/src/__tests__/errorCatching/node.spec.js index 4e3de6657..c270d68e4 100644 --- a/src/__tests__/errorCatching/node.spec.js +++ b/src/__tests__/errorCatching/node.spec.js @@ -24,7 +24,8 @@ const settings = settingsFactory({ fetchMock.get(url(settings, '/splitChanges?since=-1'), { status: 200, body: splitChangesMock1 }, responseDelay); fetchMock.get(url(settings, '/splitChanges?since=1500492097547'), { status: 200, body: splitChangesMock2 }, responseDelay); fetchMock.get(url(settings, '/splitChanges?since=1500492297547'), { status: 200, body: splitChangesMock3 }, responseDelay); - +fetchMock.postOnce(url(settings, '/v1/metrics/config'), 200); // SDK_READY +fetchMock.postOnce(url(settings, '/v1/metrics/usage'), 200); // SDK destroyed tape('Error catching on callbacks', assert => { const assertionsPlanned = 3; @@ -40,7 +41,7 @@ tape('Error catching on callbacks', assert => { scheduler: { featuresRefreshRate: 2, segmentsRefreshRate: 10000, - metricsRefreshRate: 10000, + telemetryRefreshRate: 10000, impressionsRefreshRate: 10000, eventsPushRate: 10000 }, diff --git a/src/__tests__/node.spec.js b/src/__tests__/node.spec.js index f6ec2e308..d00c76443 100644 --- a/src/__tests__/node.spec.js +++ b/src/__tests__/node.spec.js @@ -7,7 +7,7 @@ import evaluationsSuite from './nodeSuites/evaluations.spec'; import eventsSuite from './nodeSuites/events.spec'; import impressionsSuite from './nodeSuites/impressions.spec'; import impressionsSuiteDebug from './nodeSuites/impressions.debug.spec'; -// import metricsSuite from './nodeSuites/metrics.spec'; +import telemetrySuite from './nodeSuites/telemetry.spec'; import impressionsListenerSuite from './nodeSuites/impressions-listener.spec'; import expectedTreatmentsSuite from './nodeSuites/expected-treatments.spec'; import managerSuite from './nodeSuites/manager.spec'; @@ -19,13 +19,6 @@ import fetchSpecificSplits from './nodeSuites/fetch-specific-splits.spec'; import splitChangesMock1 from './mocks/splitchanges.since.-1.json'; import splitChangesMock2 from './mocks/splitchanges.since.1457552620999.json'; -const settings = settingsFactory({ - core: { - authorizationKey: '' - }, - streamingEnabled: false -}); - const config = { core: { authorizationKey: '' @@ -33,12 +26,12 @@ const config = { scheduler: { featuresRefreshRate: 1, segmentsRefreshRate: 1, - metricsRefreshRate: 3000, // for now I don't want to publish metrics during E2E run. impressionsRefreshRate: 3000 // for now I don't want to publish impressions during E2E run. }, streamingEnabled: false }; +const settings = settingsFactory(config); const key = 'facundo@split.io'; fetchMock.get(url(settings, '/splitChanges?since=-1'), { status: 200, body: splitChangesMock1 }); @@ -54,6 +47,8 @@ fetchMock.get(new RegExp(`${url(settings, '/segmentChanges')}/*`), { }); fetchMock.post(url(settings, '/testImpressions/bulk'), 200); fetchMock.post(url(settings, '/testImpressions/count'), 200); +fetchMock.post(url(settings, '/v1/metrics/config'), 200); +fetchMock.post(url(settings, '/v1/metrics/usage'), 200); tape('## Node JS - E2E CI Tests ##', async function (assert) { /* Check client evaluations. */ @@ -64,9 +59,8 @@ tape('## Node JS - E2E CI Tests ##', async function (assert) { assert.test('E2E / Impressions Debug Mode', impressionsSuiteDebug.bind(null, key, fetchMock)); assert.test('E2E / Impressions listener', impressionsListenerSuite); - // /* Check metrics */ - // @TODO uncomment when telemetry is implemented - // assert.test('E2E / Metrics', metricsSuite.bind(null, key, fetchMock)); + /* Check telemetry */ + assert.test('E2E / Telemetry', telemetrySuite.bind(null, key, fetchMock)); /* Check events in memory */ assert.test('E2E / Events', eventsSuite.bind(null, fetchMock)); diff --git a/src/__tests__/nodeSuites/events.spec.js b/src/__tests__/nodeSuites/events.spec.js index 0accdc63f..062e96560 100644 --- a/src/__tests__/nodeSuites/events.spec.js +++ b/src/__tests__/nodeSuites/events.spec.js @@ -17,7 +17,7 @@ const baseSettings = { scheduler: { featuresRefreshRate: 1, segmentsRefreshRate: 1, - metricsRefreshRate: 3000, + telemetryRefreshRate: 3000, impressionsRefreshRate: 3000, eventsPushRate: 3000 }, diff --git a/src/__tests__/nodeSuites/impressions-listener.spec.js b/src/__tests__/nodeSuites/impressions-listener.spec.js index 3c6888724..fe26fb364 100644 --- a/src/__tests__/nodeSuites/impressions-listener.spec.js +++ b/src/__tests__/nodeSuites/impressions-listener.spec.js @@ -20,11 +20,6 @@ const config = { scheduler: { featuresRefreshRate: 1, segmentsRefreshRate: 1, - metricsRefreshRate: 3000, - impressionsRefreshRate: 3000 - }, - startup: { - eventsFirstPushWindow: 3000 }, impressionListener: listener, streamingEnabled: false diff --git a/src/__tests__/nodeSuites/impressions.debug.spec.js b/src/__tests__/nodeSuites/impressions.debug.spec.js index e43fe62e6..2005d96c4 100644 --- a/src/__tests__/nodeSuites/impressions.debug.spec.js +++ b/src/__tests__/nodeSuites/impressions.debug.spec.js @@ -25,7 +25,6 @@ const config = { scheduler: { featuresRefreshRate: 1, segmentsRefreshRate: 1, - metricsRefreshRate: 3000, impressionsRefreshRate: 3000, impressionsQueueSize: 3 // flush impressions when 3 are queued }, diff --git a/src/__tests__/nodeSuites/impressions.spec.js b/src/__tests__/nodeSuites/impressions.spec.js index 6b8121018..21324b560 100644 --- a/src/__tests__/nodeSuites/impressions.spec.js +++ b/src/__tests__/nodeSuites/impressions.spec.js @@ -27,13 +27,9 @@ const config = { scheduler: { featuresRefreshRate: 1, segmentsRefreshRate: 1, - metricsRefreshRate: 3000, impressionsRefreshRate: 5 }, urls: baseUrls, - startup: { - eventsFirstPushWindow: 3000 - }, streamingEnabled: false }; diff --git a/src/__tests__/nodeSuites/ip-addresses-setting.debug.spec.js b/src/__tests__/nodeSuites/ip-addresses-setting.debug.spec.js index b6198bf71..61ae5f37c 100644 --- a/src/__tests__/nodeSuites/ip-addresses-setting.debug.spec.js +++ b/src/__tests__/nodeSuites/ip-addresses-setting.debug.spec.js @@ -3,7 +3,7 @@ import ipFunction from '../../utils/ip'; import { SplitFactory } from '../../'; import { settingsFactory } from '../../settings'; import splitChangesMock1 from '../mocks/splitchanges.since.-1.json'; -import { DEBUG, STANDALONE_MODE } from '@splitsoftware/splitio-commons/src/utils/constants'; +import { DEBUG } from '@splitsoftware/splitio-commons/src/utils/constants'; import { url } from '../testUtils'; // Header keys and expected values. Expected values are obtained with the runtime function evaluated with IPAddressesEnabled in true. @@ -14,12 +14,6 @@ const HOSTNAME_VALUE = osFunction.hostname(); // Refresh rates are set to 1 second to finish the test quickly. Otherwise, it would finish in 1 minute (60 seconds is the default value) const baseConfig = { - mode: STANDALONE_MODE, - scheduler: { - metricsRefreshRate: 1, - impressionsRefreshRate: 1, - eventsPushRate: 1 - }, streamingEnabled: false, sync: { impressionsMode: DEBUG, @@ -30,16 +24,16 @@ const baseConfig = { }, urls: { sdk: 'https://sdk.split-debug.io/api', - events: 'https://events.split-debug.io/api' + events: 'https://events.split-debug.io/api', + telemetry: 'https://telemetry.split-debug.io/api' } }; const postEndpoints = [ '/events/bulk', '/testImpressions/bulk', - // @TODO uncomment when telemetry is implemented - // '/metrics/times', - // '/metrics/counters' + '/v1/metrics/usage', + '/v1/metrics/config' ]; export default function ipAddressesSettingAssertions(fetchMock, assert) { @@ -63,7 +57,12 @@ export default function ipAddressesSettingAssertions(fetchMock, assert) { assertImpression(impression); } }; - const splitio = SplitFactory(config); + const splitio = SplitFactory(config, ({ settings }) => { + // Refresh rates are set to 1 second (below minimum values) to finish the test quickly. Otherwise, it would finish in 1 minute (60 seconds is the default value) + settings.scheduler.impressionsRefreshRate = 1000; + settings.scheduler.eventsPushRate = 1000; + settings.scheduler.telemetryRefreshRate = 1000; + }); const client = splitio.client(); const settings = settingsFactory(config); diff --git a/src/__tests__/nodeSuites/ip-addresses-setting.spec.js b/src/__tests__/nodeSuites/ip-addresses-setting.spec.js index 6df0b6209..eb7a4292c 100644 --- a/src/__tests__/nodeSuites/ip-addresses-setting.spec.js +++ b/src/__tests__/nodeSuites/ip-addresses-setting.spec.js @@ -13,51 +13,44 @@ const IP_VALUE = ipFunction.address(); const HOSTNAME_VALUE = osFunction.hostname(); const NA = 'NA'; -// Refresh rates are set to 1 second to finish the test quickly. Otherwise, it would finish in 1 minute (60 seconds is the default value) -const baseConfig = { - scheduler: { - metricsRefreshRate: 1, - impressionsRefreshRate: 1, - eventsPushRate: 1 - }, - streamingEnabled: false -}; - // Config with IPAddressesEnabled set to false const configWithIPAddressesDisabled = { - ...baseConfig, + streamingEnabled: false, core: { authorizationKey: '', IPAddressesEnabled: false }, urls: { sdk: 'https://sdk.split-ipdisabled.io/api', - events: 'https://events.split-ipdisabled.io/api' + events: 'https://events.split-ipdisabled.io/api', + telemetry: 'https://telemetry.split-ipdisabled.io/api', } }; // Config with IPAddressesEnabled set to true const configWithIPAddressesEnabled = { - ...baseConfig, + streamingEnabled: false, core: { authorizationKey: '', IPAddressesEnabled: true }, urls: { sdk: 'https://sdk.split-ipenabled.io/api', - events: 'https://events.split-ipenabled.io/api' + events: 'https://events.split-ipenabled.io/api', + telemetry: 'https://telemetry.split-ipenabled.io/api' } }; // Config with default IPAddressesEnabled (true) const configWithIPAddressesDefault = { - ...baseConfig, + streamingEnabled: false, core: { authorizationKey: '' }, urls: { sdk: 'https://sdk.split-ipdefault.io/api', - events: 'https://events.split-ipdefault.io/api' + events: 'https://events.split-ipdefault.io/api', + telemetry: 'https://telemetry.split-ipdefault.io/api' } }; @@ -70,9 +63,8 @@ const configSamples = [ const postEndpoints = [ '/events/bulk', '/testImpressions/bulk', - // @TODO uncomment when telemetry is implemented - // '/metrics/times', - // '/metrics/counters' + '/v1/metrics/usage', + '/v1/metrics/config' ]; export default function ipAddressesSettingAssertions(fetchMock, assert) { @@ -109,7 +101,12 @@ export default function ipAddressesSettingAssertions(fetchMock, assert) { assertImpression(config.core.IPAddressesEnabled === undefined ? true : config.core.IPAddressesEnabled, config.mode === undefined ? STANDALONE_MODE : config.mode, impression); } }; - const splitio = SplitFactory(config); + const splitio = SplitFactory(config, ({ settings }) => { + // Refresh rates are set to 1 second (below minimum values) to finish the test quickly. Otherwise, it would finish in 1 minute (60 seconds is the default value) + settings.scheduler.impressionsRefreshRate = 1000; + settings.scheduler.eventsPushRate = 1000; + settings.scheduler.telemetryRefreshRate = 1000; + }); const client = splitio.client(); const settings = settingsFactory(config); diff --git a/src/__tests__/nodeSuites/metrics.spec.js b/src/__tests__/nodeSuites/metrics.spec.js deleted file mode 100644 index 9cfebcd9e..000000000 --- a/src/__tests__/nodeSuites/metrics.spec.js +++ /dev/null @@ -1,173 +0,0 @@ -import { SplitFactory } from '../../'; -import { settingsFactory } from '../../settings'; -import splitChangesMock1 from '../mocks/splitchanges.since.-1.json'; -import splitChangesMock2 from '../mocks/splitchanges.since.1457552620999.json'; -import { OPTIMIZED } from '@splitsoftware/splitio-commons/src/utils/constants'; -import { url } from '../testUtils'; - -const baseUrls = { - sdk: 'https://sdk.baseurl/metricsSuite', - events: 'https://events.baseurl/metricsSuite' -}; - -const settings = settingsFactory({ - core: { - key: '' - }, - urls: baseUrls, - streamingEnabled: false -}); - -const config = { - core: { - authorizationKey: '' - }, - scheduler: { - featuresRefreshRate: 99999, - segmentsRefreshRate: 99999, - metricsRefreshRate: 3, - impressionsRefreshRate: 99999 - }, - urls: baseUrls, - startup: { - eventsFirstPushWindow: 3000 - }, - streamingEnabled: false -}; - -export default async function(key, fetchMock, assert) { - const segmentChangesUrlRegex = new RegExp(`${baseUrls.sdk}/segmentChanges/*`); - - fetchMock.getOnce(url(settings, '/splitChanges?since=-1'), 500); - fetchMock.getOnce(url(settings, '/splitChanges?since=-1'), { status: 200, body: splitChangesMock1 }); - fetchMock.getOnce(segmentChangesUrlRegex, { status: 200, body: { since:10, till:10, name: 'segmentName', added: [], removed: [] } }); - fetchMock.getOnce(segmentChangesUrlRegex, 401); - fetchMock.getOnce(segmentChangesUrlRegex, 500); - fetchMock.getOnce(segmentChangesUrlRegex, { status: 200, body: '{ INVALID JSON' }); - fetchMock.get(segmentChangesUrlRegex, { status: 200, body: {since:10, till:10, name: 'segmentName' + Date.now(), added: [], removed: []} }); - // Should not execute but adding just in case. - fetchMock.get(url(settings, '/splitChanges?since=1457552620999'), { status: 200, body: splitChangesMock2 }); - - fetchMock.postOnce(url(settings, '/testImpressions/bulk'), (url, opts) => { - assert.equal(opts.headers.SplitSDKImpressionsMode, OPTIMIZED); - return 200; - }); - fetchMock.postOnce(url(settings, '/testImpressions/count'), 200); - - const splitio = SplitFactory(config); - const client = splitio.client(); - - const finish = (function*() { - yield; - client.destroy(); - assert.end(); - })(); - - fetchMock.postOnce(url(settings, '/metrics/times'), (url, opts) => { - const data = JSON.parse(opts.body); - - assert.equal(data.length, 7, 'We performed 4 correct evaluation requests (one per method) plus ready, splits and segments, so we should have 7 latency metrics.'); - - const latencyMetricsRecorded = data.filter(metric => { - // At least one latency registed per metric - return metric.latencies.some(count => count > 0); - }); - - // If we have 7 items, it is because each one had at least ONE entry. - assert.equal(latencyMetricsRecorded.length, 7, 'Each metric has at least one enty, matching the calls.'); - - const getLatencyCount = metricName => { - const latencyMetric = data.find(metric => metric.name === metricName); - - if (!latencyMetric && !Array.isArray(latencyMetric.latencies)) return 0; - - return latencyMetric.latencies.reduce((accum, entry) => accum + entry, 0); - }; - - // Validate both names and values. - assert.equal(getLatencyCount('splitChangeFetcher.time'), 2, 'Two latency metrics for splitChanges'); - assert.equal(getLatencyCount('segmentChangeFetcher.time'), 1, 'One latency metric for segmentChangeFetcher'); - assert.equal(getLatencyCount('sdk.ready'), 1, 'One latency metric for ready'); - assert.equal(getLatencyCount('sdk.getTreatment'), 1, 'One latency metric for getTreatment'); - assert.equal(getLatencyCount('sdk.getTreatments'), 1, 'One latency metric for getTreatments'); - assert.equal(getLatencyCount('sdk.getTreatmentWithConfig'), 1, 'One latency metric for getTreatmentWithConfig'); - assert.equal(getLatencyCount('sdk.getTreatmentsWithConfig'), 1, 'One latency metric for getTreatmentsWithConfig'); - - finish.next(); - - return 200; - }); - - fetchMock.postOnce(url(settings, '/metrics/counters'), (url, opts) => { - const data = JSON.parse(opts.body); - - assert.equal(data.length, 4, 'Based on the mock setup, we should have four items.'); - - const countMetricsRecorded = data.reduce((accum, metric) => accum + metric.delta, 0); - - const getRecordsCount = metricName => { - const countMetric = data.find(metric => metric.name === metricName); - - if (!countMetric) return 0; - - return countMetric.delta; - }; - - // 5 items: - // For splitChanges, 1 exception and 1 200. - // For segmentChanges (3 segments) 1 with 200, two errors. - assert.equal(countMetricsRecorded, 5, 'Each metric has one entry, same as the amount of calls.'); - // break down - assert.equal(getRecordsCount('splitChangeFetcher.exception'), 1, 'The metric names and delta should correspond to the SDK behaviour.'); - assert.equal(getRecordsCount('segmentChangeFetcher.exception'), 2, 'The metric names and delta should correspond to the SDK behaviour.'); - assert.equal(getRecordsCount('splitChangeFetcher.status.200'), 1, 'The metric names and delta should correspond to the SDK behaviour.'); - assert.equal(getRecordsCount('segmentChangeFetcher.status.200'), 1, 'The metric names and delta should correspond to the SDK behaviour.'); - - finish.next(); - - return 200; - }); - - await client.ready(); - - // treatments and results are only validated so we know for sure when the function was actually running to compare the metrics. - assert.equal(client.getTreatment(key, 'always_on'), 'on', 'Evaluation was correct.'); - assert.equal(client.getTreatment(false, 'always_on'), 'control', 'We should return control with invalid input.'); - - assert.deepEqual(client.getTreatmentWithConfig(key, 'split_with_config'), { - treatment: 'o.n', - config: '{"color":"brown","dimensions":{"height":12,"width":14},"text":{"inner":"click me"}}' - }, 'Evaluation with config was correct.'); - assert.deepEqual(client.getTreatmentWithConfig(null, 'split_with_config'), { - treatment: 'control', - config: null - }, 'Evaluation with config returned control state for invalid input.'); - - assert.deepEqual(client.getTreatments(key, ['always_on', 'always_off']), { always_on:'on', always_off:'off' }, 'Evaluations were correct.'); - assert.deepEqual(client.getTreatments(false, ['always_on', 'always_off', null]), { always_on:'control', always_off:'control' }, 'We should return map of controls with invalid input.'); - - assert.deepEqual(client.getTreatmentsWithConfig(key, ['split_with_config', 'always_on', null]), - { - split_with_config: { - treatment: 'o.n', - config: '{"color":"brown","dimensions":{"height":12,"width":14},"text":{"inner":"click me"}}' - }, - always_on: { - treatment: 'on', - config: null - } - } - , 'Evaluations with config were correct.'); - assert.deepEqual(client.getTreatmentsWithConfig(null, ['split_with_config', 'always_on', null]), - { - split_with_config: { - treatment: 'control', - config: null - }, - always_on: { - treatment: 'control', - config: null - } - }, - 'Evaluations with config returned control states for invalid input.'); -} diff --git a/src/__tests__/nodeSuites/push-fallbacking.spec.js b/src/__tests__/nodeSuites/push-fallbacking.spec.js index de33fe7a7..8285028ad 100644 --- a/src/__tests__/nodeSuites/push-fallbacking.spec.js +++ b/src/__tests__/nodeSuites/push-fallbacking.spec.js @@ -46,8 +46,6 @@ const config = { scheduler: { featuresRefreshRate: 0.2, segmentsRefreshRate: 0.25, - metricsRefreshRate: 3000, - impressionsRefreshRate: 3000 }, urls: baseUrls, streamingEnabled: true, diff --git a/src/__tests__/nodeSuites/push-initialization-nopush.spec.js b/src/__tests__/nodeSuites/push-initialization-nopush.spec.js index 80e6972a8..f17b41412 100644 --- a/src/__tests__/nodeSuites/push-initialization-nopush.spec.js +++ b/src/__tests__/nodeSuites/push-initialization-nopush.spec.js @@ -23,14 +23,9 @@ const config = { scheduler: { featuresRefreshRate: 0.1, segmentsRefreshRate: 0.1, - metricsRefreshRate: 3000, - impressionsRefreshRate: 3000, pushRetryBackoffBase: 0.01 // small value to assert rapidly that push is not retried }, urls: baseUrls, - startup: { - eventsFirstPushWindow: 3000 - }, streamingEnabled: true, // debug: true, }; diff --git a/src/__tests__/nodeSuites/push-initialization-retries.spec.js b/src/__tests__/nodeSuites/push-initialization-retries.spec.js index 5f6dfb762..51b7b0ece 100644 --- a/src/__tests__/nodeSuites/push-initialization-retries.spec.js +++ b/src/__tests__/nodeSuites/push-initialization-retries.spec.js @@ -24,14 +24,9 @@ const config = { scheduler: { featuresRefreshRate: 0.2, segmentsRefreshRate: 0.2, - metricsRefreshRate: 3000, - impressionsRefreshRate: 3000, pushRetryBackoffBase: 0.1 }, urls: baseUrls, - startup: { - eventsFirstPushWindow: 3000 - }, streamingEnabled: true, // debug: true, }; diff --git a/src/__tests__/nodeSuites/push-refresh-token.spec.js b/src/__tests__/nodeSuites/push-refresh-token.spec.js index 546c7a992..644d2c07d 100644 --- a/src/__tests__/nodeSuites/push-refresh-token.spec.js +++ b/src/__tests__/nodeSuites/push-refresh-token.spec.js @@ -45,6 +45,8 @@ const MILLIS_REFRESH_TOKEN = 1000; */ export function testRefreshToken(fetchMock, assert) { fetchMock.reset(); + fetchMock.postOnce('https://telemetry.split.io/api/v1/metrics/config', 200); + fetchMock.postOnce('https://telemetry.split.io/api/v1/metrics/usage', 200); __setEventSource(EventSourceMock); let start, splitio, client; diff --git a/src/__tests__/nodeSuites/telemetry.spec.js b/src/__tests__/nodeSuites/telemetry.spec.js new file mode 100644 index 000000000..81679fc81 --- /dev/null +++ b/src/__tests__/nodeSuites/telemetry.spec.js @@ -0,0 +1,158 @@ +import { SplitFactory } from '../../'; +import splitChangesMock1 from '../mocks/splitchanges.since.-1.json'; +import { url, mockSegmentChanges } from '../testUtils'; + +const baseUrls = { + sdk: 'https://sdk.baseurl/telemetrySuite', + events: 'https://events.baseurl/telemetrySuite', + telemetry: 'https://telemetry.baseurl/telemetrySuite', +}; + +const config = { + core: { + authorizationKey: '' + }, + scheduler: { + featuresRefreshRate: 99999, + telemetryRefreshRate: 60 + }, + urls: baseUrls, + streamingEnabled: false +}; + +export default async function telemetryNodejsSuite(key, fetchMock, assert) { + + fetchMock.getOnce(url(config, '/splitChanges?since=-1'), 500); // record http exception + fetchMock.getOnce(url(config, '/splitChanges?since=-1'), { status: 200, body: splitChangesMock1 }); + mockSegmentChanges(fetchMock, new RegExp(config.urls.sdk + '/segmentChanges/*'), ['some_key']); + + fetchMock.postOnce(url(config, '/testImpressions/bulk'), 200); + fetchMock.postOnce(url(config, '/testImpressions/count'), 200); + fetchMock.postOnce(url(config, '/events/bulk'), 200); + + const splitio = SplitFactory(config, ({ settings }) => { + assert.equal(settings.scheduler.telemetryRefreshRate, 60000); + settings.scheduler.telemetryRefreshRate = 1000; // set below minimum to validate matrics/usage requests + }); + const client = splitio.client(); + + const finish = (function* () { + yield; + client.destroy(); + assert.end(); + })(); + + let lastSync; + + // 1st metrics/usage call due to telemetryRefreshRate set in 1 second + fetchMock.postOnce(url(config, '/v1/metrics/usage'), (url, opts) => { + const data = JSON.parse(opts.body); + + // Validate last successful sync + assert.deepEqual(Object.keys(data.lS), ['sp', 'se', 'te'], 'Successful split changes, segment changes and telemetry config requests'); + lastSync = data.lS; delete data.lS; + + // Validate http and method latencies + const getLatencyCount = buckets => buckets ? buckets.reduce((accum, entry) => accum + entry, 0) : 0; + assert.equal(getLatencyCount(data.hL.sp), 2, 'Two latency metrics for splitChanges GET request'); + assert.equal(getLatencyCount(data.hL.se), 6, 'Six latency metrics for segmentChanges GET request'); + assert.equal(getLatencyCount(data.hL.te), 1, 'One latency metric for telemetry config POST request'); + assert.equal(getLatencyCount(data.mL.t), 2, 'Two latency metrics for getTreatment (one not ready usage'); + assert.equal(getLatencyCount(data.mL.ts), 1, 'One latency metric for getTreatments'); + assert.equal(getLatencyCount(data.mL.tc), 1, 'One latency metric for getTreatmentWithConfig'); + assert.equal(getLatencyCount(data.mL.tcs), 1, 'One latency metric for getTreatmentsWithConfig'); + assert.equal(getLatencyCount(data.mL.tr), 1, 'One latency metric for track'); + delete data.hL; delete data.mL; + + assert.deepEqual(data, { + mE: {}, hE: { sp: { 500: 1 } }, tR: 0, aR: 0, iQ: 4, iDe: 3, iDr: 0, spC: 31, seC: 3, skC: 3, eQ: 1, eD: 0, sE: [], t: [] + }, 'metrics/usage JSON payload should be the expected'); + + finish.next(); + return 200; + }); + + // 2nd metrics/usage call due to destroy + fetchMock.postOnce(url(config, '/v1/metrics/usage'), (url, opts) => { + const data = JSON.parse(opts.body); + + assert.deepEqual(data.lS, lastSync, 'last successful sync hasn\'t change'); + delete data.lS; + + assert.true(data.sL > 0, 'sessionLengthMs must be defined'); + delete data.sL; + + assert.deepEqual(data, { + mL: {}, mE: {}, hE: {}, hL: {}, // errors and latencies were popped + tR: 0, aR: 0, iQ: 4, iDe: 3, iDr: 0, spC: 31, seC: 3, skC: 3, eQ: 1, eD: 0, sE: [], t: [] + }, '2nd metrics/usage JSON payload should be the expected'); + return 200; + }); + + fetchMock.postOnce(url(config, '/v1/metrics/config'), (url, opts) => { + const data = JSON.parse(opts.body); + + assert.true(data.tR > 0, 'timeUntilReady is larger than 0'); + delete data.tR; // delete to validate other properties + + assert.deepEqual(data, { + oM: 0, st: 'memory', aF: 1, rF: 0, sE: false, + rR: { sp: 99999, se: 60, im: 300, ev: 60, te: 1 } /* override featuresRefreshRate */, + uO: { s: true, e: true, a: false, st: false, t: true } /* override sdk, events and telemetry URLs */, + iQ: 30000, eQ: 500, iM: 0, iL: false, hP: false, nR: 1 /* 1 non ready usage */, t: [], uC: 0 /* NA */ + }, 'metrics/config JSON payload should be the expected'); + + finish.next(); + + return 200; + }); + + assert.equal(client.getTreatment(key, 'always_on'), 'control', 'Non ready usage.'); + + await client.ready(); + + // treatments and results are only validated so we know for sure when the function was actually running to compare the metrics. + assert.equal(client.getTreatment(key, 'always_on'), 'on', 'Evaluation was correct.'); + assert.equal(client.getTreatment(false, 'always_on'), 'control', 'We should return control with invalid input.'); + + assert.deepEqual(client.getTreatmentWithConfig(key, 'split_with_config'), { + treatment: 'o.n', + config: '{"color":"brown","dimensions":{"height":12,"width":14},"text":{"inner":"click me"}}' + }, 'Evaluation with config was correct.'); + assert.deepEqual(client.getTreatmentWithConfig(null, 'split_with_config'), { + treatment: 'control', + config: null + }, 'Evaluation with config returned control state for invalid input.'); + + assert.deepEqual(client.getTreatments(key, ['always_on', 'always_off']), { always_on: 'on', always_off: 'off' }, 'Evaluations were correct.'); + assert.deepEqual(client.getTreatments(false, ['always_on', 'always_off', null]), { always_on: 'control', always_off: 'control' }, 'We should return map of controls with invalid input.'); + + assert.deepEqual(client.getTreatmentsWithConfig(key, ['split_with_config', 'always_on', null]), + { + split_with_config: { + treatment: 'o.n', + config: '{"color":"brown","dimensions":{"height":12,"width":14},"text":{"inner":"click me"}}' + }, + always_on: { + treatment: 'on', + config: null + } + } + , 'Evaluations with config were correct.'); + assert.deepEqual(client.getTreatmentsWithConfig(null, ['split_with_config', 'always_on', null]), + { + split_with_config: { + treatment: 'control', + config: null + }, + always_on: { + treatment: 'control', + config: null + } + }, + 'Evaluations with config returned control states for invalid input.'); + + assert.equal(client.track(key, 'someTT', 'someEvent'), true, 'Event was queued'); + assert.equal(client.track(false, 'someTT', 'someEvent'), false, 'Invalid input.'); + +} diff --git a/src/__tests__/node_redis.spec.js b/src/__tests__/node_redis.spec.js index 18edf10fb..2ee60f4b8 100644 --- a/src/__tests__/node_redis.spec.js +++ b/src/__tests__/node_redis.spec.js @@ -13,6 +13,7 @@ import { KeyBuilderSS } from '@splitsoftware/splitio-commons/src/storages/KeyBui import { validatePrefix } from '@splitsoftware/splitio-commons/src/storages/KeyBuilder'; import { settingsFactory } from '../settings'; import { nearlyEqual } from './testUtils'; +import { version } from '../../package.json'; const IP_VALUE = ipFunction.address(); const HOSTNAME_VALUE = osFunction.hostname(); @@ -119,13 +120,26 @@ tape('NodeJS Redis', function (t) { await client.ready(); // promise already resolved await client.destroy(); + // Validate stored impressions and events exec(`echo "LLEN ${config.storage.prefix}.SPLITIO.impressions \n LLEN ${config.storage.prefix}.SPLITIO.events" | redis-cli -p ${redisPort}`, (error, stdout) => { if (error) assert.fail('Redis server should be reachable'); const trackedImpressionsAndEvents = stdout.split('\n').filter(line => line !== '').map(line => parseInt(line)); assert.deepEqual(trackedImpressionsAndEvents, [14, 2], 'Tracked impressions and events should be stored in Redis'); - // close server connection - server.close().then(assert.end); + + // Validate stored telemetry + exec(`echo "HLEN ${config.storage.prefix}.SPLITIO.telemetry.latencies \n HLEN ${config.storage.prefix}.SPLITIO.telemetry.exceptions \n HGET ${config.storage.prefix}.SPLITIO.telemetry.init nodejs-${version}/${HOSTNAME_VALUE}/${IP_VALUE}" | redis-cli -p ${redisPort}`, (error, stdout) => { + if (error) assert.fail('Redis server should be reachable'); + + const [latencies, exceptions, configValue] = stdout.split('\n').filter(line => line !== '').map(JSON.parse); + + assert.true(latencies > 0, 'There are stored latencies'); + assert.true(exceptions === 0, 'There aren\'t stored exceptions'); + assert.deepEqual(configValue, { oM: 1, st: 'redis', aF: 1, rF: 0 }, 'There is stored telemetry config'); + + // close server connection + server.close().then(assert.end); + }); }); }); }); diff --git a/src/__tests__/offline/browser.spec.js b/src/__tests__/offline/browser.spec.js index e3f8552aa..b97496a50 100644 --- a/src/__tests__/offline/browser.spec.js +++ b/src/__tests__/offline/browser.spec.js @@ -56,7 +56,6 @@ tape('Browser offline mode', function (assert) { scheduler: { impressionsRefreshRate: 0.01, eventsPushRate: 0.01, - metricsRefreshRate: 0.01, offlineRefreshRate: 0.19 }, startup: { diff --git a/src/__tests__/offline/node.spec.js b/src/__tests__/offline/node.spec.js index cd4bb3630..777c7649d 100644 --- a/src/__tests__/offline/node.spec.js +++ b/src/__tests__/offline/node.spec.js @@ -46,11 +46,9 @@ const settingsGenerator = mockFileName => { scheduler: { impressionsRefreshRate: 0.01, eventsPushRate: 0.01, - metricsRefreshRate: 0.01, offlineRefreshRate: 0.3 }, startup: { - eventsFirstPushWindow: 0, readyTimeout: 3, retriesOnFailureBeforeReady: 0 }, diff --git a/src/__tests__/push/node.spec.js b/src/__tests__/push/node.spec.js index a41673d49..14d6b59c7 100644 --- a/src/__tests__/push/node.spec.js +++ b/src/__tests__/push/node.spec.js @@ -9,6 +9,9 @@ import { testRefreshToken } from '../nodeSuites/push-refresh-token.spec'; fetchMock.config.overwriteRoutes = false; +fetchMock.post('https://telemetry.split.io/api/v1/metrics/config', 200); +fetchMock.post('https://telemetry.split.io/api/v1/metrics/usage', 200); + tape('## Node JS - E2E CI Tests for PUSH ##', async function (assert) { // Non-recoverable issues on inizialization diff --git a/src/__tests__/testUtils/index.js b/src/__tests__/testUtils/index.js index 885bcd1c1..302e9449d 100644 --- a/src/__tests__/testUtils/index.js +++ b/src/__tests__/testUtils/index.js @@ -1,7 +1,7 @@ const DEFAULT_ERROR_MARGIN = 50; // 0.05 secs /** - * Assert if an `actual` and `expected` numeric values are nearlyEqual. + * Assert if an `actual` and `expected` numeric values are nearly equal. * * @param {number} actual actual time lapse in millis * @param {number} expected expected time lapse in millis @@ -43,6 +43,7 @@ export function hasNoCacheHeader(fetchMockOpts) { return fetchMockOpts.headers['Cache-Control'] === 'no-cache'; } +const telemetryEndpointMatcher = /^\/v1\/metrics\/(config|usage)/; const eventsEndpointMatcher = /^\/(testImpressions|metrics|events)/; const authEndpointMatcher = /^\/v2\/auth/; const streamingEndpointMatcher = /^\/(sse|event-stream)/; @@ -56,6 +57,9 @@ const streamingEndpointMatcher = /^\/(sse|event-stream)/; * @return {String} completed url */ export function url(settings, target) { + if (telemetryEndpointMatcher.test(target)) { + return `${settings.urls.telemetry}${target}`; + } if (eventsEndpointMatcher.test(target)) { return `${settings.urls.events}${target}`; } diff --git a/src/platform/browser.js b/src/platform/browser.js index feec1a7ad..9432f1d60 100644 --- a/src/platform/browser.js +++ b/src/platform/browser.js @@ -2,11 +2,13 @@ import { EventEmitter } from './EventEmitter'; import { getFetch } from '../platform/getFetch/browser'; import { getEventSource } from '../platform/getEventSource/browser'; import { BrowserSignalListener } from '@splitsoftware/splitio-commons/src/listeners/browser'; +import { now } from '@splitsoftware/splitio-commons/src/utils/timeTracker/now/browser'; export const platform = { getFetch, getEventSource, - EventEmitter + EventEmitter, + now }; export const SignalListener = BrowserSignalListener; diff --git a/src/platform/node.js b/src/platform/node.js index 5fb850afa..d3d2a93bb 100644 --- a/src/platform/node.js +++ b/src/platform/node.js @@ -3,12 +3,14 @@ import { getFetch } from '../platform/getFetch/node'; import { getEventSource } from '../platform/getEventSource/node'; import { getOptions } from '../platform/request/options/node'; import { NodeSignalListener } from '@splitsoftware/splitio-commons/src/listeners/node'; +import { now } from '@splitsoftware/splitio-commons/src/utils/timeTracker/now/node'; export const platform = { getOptions, getFetch, getEventSource, - EventEmitter + EventEmitter, + now }; export const SignalListener = NodeSignalListener; diff --git a/src/settings/defaults/node.js b/src/settings/defaults/node.js index 4087b5344..25b8e3894 100644 --- a/src/settings/defaults/node.js +++ b/src/settings/defaults/node.js @@ -16,8 +16,7 @@ export const defaults = { eventsFirstPushWindow: 0 }, scheduler: { - featuresRefreshRate: 5, - impressionsRefreshRate: 300 + featuresRefreshRate: 5 }, features: '.split', diff --git a/src/settings/defaults/version.js b/src/settings/defaults/version.js index 3187bdd08..87675e575 100644 --- a/src/settings/defaults/version.js +++ b/src/settings/defaults/version.js @@ -1 +1 @@ -export const packageVersion = '10.18.2'; +export const packageVersion = '10.19.0'; diff --git a/ts-tests/index.ts b/ts-tests/index.ts index 0f2d111d4..27926f4fc 100644 --- a/ts-tests/index.ts +++ b/ts-tests/index.ts @@ -478,6 +478,7 @@ let fullBrowserSettings: SplitIO.IBrowserSettings = { impressionsRefreshRate: 1, impressionsQueueSize: 1, metricsRefreshRate: 1, + telemetryRefreshRate: 1, segmentsRefreshRate: 1, offlineRefreshRate: 1, eventsPushRate: 1, @@ -494,7 +495,8 @@ let fullBrowserSettings: SplitIO.IBrowserSettings = { sdk: 'https://asd.com/sdk', events: 'https://asd.com/events', auth: 'https://asd.com/auth', - streaming: 'https://asd.com/streaming' + streaming: 'https://asd.com/streaming', + telemetry: 'https://asd.com/telemetry' }, features: mockedFeaturesMap, storage: { @@ -527,6 +529,7 @@ let fullNodeSettings: SplitIO.INodeSettings = { impressionsRefreshRate: 1, impressionsQueueSize: 1, metricsRefreshRate: 1, + telemetryRefreshRate: 1, segmentsRefreshRate: 1, offlineRefreshRate: 1, eventsPushRate: 1, @@ -543,7 +546,8 @@ let fullNodeSettings: SplitIO.INodeSettings = { sdk: 'https://asd.com/sdk', events: 'https://asd.com/events', auth: 'https://asd.com/auth', - streaming: 'https://asd.com/streaming' + streaming: 'https://asd.com/streaming', + telemetry: 'https://asd.com/telemetry' }, features: mockedFeaturesPath, storage: { @@ -572,6 +576,7 @@ let fullAsyncSettings: SplitIO.INodeAsyncSettings = { impressionsRefreshRate: 1, impressionsQueueSize: 1, metricsRefreshRate: 1, + telemetryRefreshRate: 1, segmentsRefreshRate: 1, offlineRefreshRate: 1, eventsPushRate: 1, diff --git a/ts-tests/package.json b/ts-tests/package.json index 817b0252d..8d6336589 100644 --- a/ts-tests/package.json +++ b/ts-tests/package.json @@ -6,7 +6,7 @@ "license": "Apache-2.0", "repository": "splitio/javascript-client", "dependencies": { - "@types/node": "^14.18.12", + "@types/node": "^14.18.16", "typescript": "^3.7.4" } } diff --git a/types/splitio.d.ts b/types/splitio.d.ts index 53503861a..7a3a44398 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -71,7 +71,11 @@ interface ISettings { featuresRefreshRate: number, impressionsRefreshRate: number, impressionsQueueSize: number, - metricsRefreshRate: number, + /** + * @deprecated + */ + metricsRefreshRate?: number, + telemetryRefreshRate: number, segmentsRefreshRate: number, offlineRefreshRate: number, eventsPushRate: number, @@ -93,7 +97,8 @@ interface ISettings { events: string, sdk: string, auth: string, - streaming: string + streaming: string, + telemetry: string }, readonly debug: boolean | LogLevel, readonly version: string, @@ -297,8 +302,15 @@ interface INodeBasicSettings extends ISharedSettings { * The SDK sends diagnostic metrics to Split servers. This parameters controls this metric flush period in seconds. * @property {number} metricsRefreshRate * @default 120 + * @deprecated This parameter is ignored now. Use `telemetryRefreshRate` instead. */ metricsRefreshRate?: number, + /** + * The SDK sends diagnostic metrics to Split servers. This parameters controls this metric flush period in seconds. + * @property {number} telemetryRefreshRate + * @default 3600 + */ + telemetryRefreshRate?: number, /** * The SDK polls Split servers for changes to segment definitions. This parameter controls this polling period in seconds. * @property {number} segmentsRefreshRate @@ -843,7 +855,13 @@ declare namespace SplitIO { * @property {string} streaming * @default 'https://streaming.split.io' */ - streaming?: string + streaming?: string, + /** + * String property to override the base URL where the SDK will post telemetry data. + * @property {string} telemetry + * @default 'https://telemetry.split.io/api' + */ + telemetry?: string }; /** @@ -947,8 +965,15 @@ declare namespace SplitIO { * The SDK sends diagnostic metrics to Split servers. This parameters controls this metric flush period in seconds. * @property {number} metricsRefreshRate * @default 120 + * @deprecated This parameter is ignored now. Use `telemetryRefreshRate` instead. */ metricsRefreshRate?: number, + /** + * The SDK sends diagnostic metrics to Split servers. This parameters controls this metric flush period in seconds. + * @property {number} telemetryRefreshRate + * @default 3600 + */ + telemetryRefreshRate?: number, /** * The SDK polls Split servers for changes to segment definitions. This parameter controls this polling period in seconds. * @property {number} segmentsRefreshRate