diff --git a/docs/3.0.0-alpha.x/guides/authentication.md b/docs/3.0.0-alpha.x/guides/authentication.md index 4abf55b835..9cd651e249 100644 --- a/docs/3.0.0-alpha.x/guides/authentication.md +++ b/docs/3.0.0-alpha.x/guides/authentication.md @@ -28,7 +28,7 @@ axios }) .catch(error => { // Handle error. - console.log('An error occurred:', error); + console.log('An error occurred:', error.response); }); ``` @@ -57,7 +57,7 @@ axios }) .catch(error => { // Handle error. - console.log('An error occurred:', error); + console.log('An error occurred:', error.response); }); ``` @@ -86,7 +86,7 @@ axios }) .catch(error => { // Handle error. - console.log('An error occurred:', error); + console.log('An error occurred:', error.response); }); ``` @@ -148,7 +148,7 @@ axios }) .catch(error => { // Handle error. - console.log('An error occurred:', error); + console.log('An error occurred:', error.response); }); ``` @@ -176,7 +176,7 @@ axios }) .catch(error => { // Handle error. - console.log('An error occurred:', error); + console.log('An error occurred:', error.response); }); }); ``` diff --git a/docs/3.0.0-beta.x/deployment/amazon-aws.md b/docs/3.0.0-beta.x/deployment/amazon-aws.md index e783e4e358..b099f265c4 100644 --- a/docs/3.0.0-beta.x/deployment/amazon-aws.md +++ b/docs/3.0.0-beta.x/deployment/amazon-aws.md @@ -211,7 +211,7 @@ Strapi currently supports `Node.js v12.x.x`. The following steps will install No ```bash cd ~ -curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash - +curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash - ... sudo apt-get install nodejs ... diff --git a/docs/3.0.0-beta.x/plugins/users-permissions.md b/docs/3.0.0-beta.x/plugins/users-permissions.md index af0783487f..89263c66fb 100644 --- a/docs/3.0.0-beta.x/plugins/users-permissions.md +++ b/docs/3.0.0-beta.x/plugins/users-permissions.md @@ -70,7 +70,7 @@ axios }) .catch(error => { // Handle error. - console.log('An error occurred:', error); + console.log('An error occurred:', error.response); }); ``` @@ -99,7 +99,7 @@ axios }) .catch(error => { // Handle error. - console.log('An error occurred:', error); + console.log('An error occurred:', error.response); }); ``` @@ -128,7 +128,7 @@ axios }) .catch(error => { // Handle error. - console.log('An error occurred:', error); + console.log('An error occurred:', error.response); }); ``` @@ -486,7 +486,7 @@ axios }) .catch(error => { // Handle error. - console.log('An error occurred:', error); + console.log('An error occurred:', error.response); }); ``` @@ -519,7 +519,7 @@ axios }) .catch(error => { // Handle error. - console.log('An error occurred:', error); + console.log('An error occurred:', error.response); }); ``` @@ -545,7 +545,7 @@ axios }) .catch(error => { // Handle error. - console.err('An error occured:', err); + console.error('An error occured:', error.response); }); ``` diff --git a/packages/strapi-plugin-content-manager/admin/src/components/Wysiwyg/index.js b/packages/strapi-plugin-content-manager/admin/src/components/Wysiwyg/index.js index dc398255fd..e43188a7e5 100644 --- a/packages/strapi-plugin-content-manager/admin/src/components/Wysiwyg/index.js +++ b/packages/strapi-plugin-content-manager/admin/src/components/Wysiwyg/index.js @@ -582,8 +582,8 @@ class Wysiwyg extends React.Component { Modifier.replaceText(contentState, this.getSelection(), text); onChange = editorState => { - this.setState({ editorState }); this.sendData(editorState); + this.setState({ editorState }); }; handleTab = e => { diff --git a/packages/strapi-plugin-upload/services/Upload.js b/packages/strapi-plugin-upload/services/Upload.js index 15bc623e82..a3793ed23f 100644 --- a/packages/strapi-plugin-upload/services/Upload.js +++ b/packages/strapi-plugin-upload/services/Upload.js @@ -23,6 +23,16 @@ const generateFileName = name => { return `${baseName}_${randomSuffix()}`; }; +const sendMediaMetrics = data => { + if (_.has(data, 'caption') && !_.isEmpty(data.caption)) { + strapi.telemetry.send('didSaveMediaWithCaption'); + } + + if (_.has(data, 'alternativeText') && !_.isEmpty(data.alternativeText)) { + strapi.telemetry.send('didSaveMediaWithAlternativeText'); + } +}; + const combineFilters = params => { // FIXME: until we support boolean operators for querying we need to make mime_ncontains use AND instead of OR if (_.has(params, 'mime_ncontains') && Array.isArray(params.mime_ncontains)) { @@ -244,12 +254,16 @@ module.exports = { }, async update(params, values) { + sendMediaMetrics(values); + const res = await strapi.query('file', 'upload').update(params, values); strapi.eventHub.emit('media.update', { media: res }); return res; }, async add(values) { + sendMediaMetrics(values); + const res = await strapi.query('file', 'upload').create(values); strapi.eventHub.emit('media.create', { media: res }); return res; @@ -335,6 +349,12 @@ module.exports = { }, setSettings(value) { + if (value.responsiveDimensions === true) { + strapi.telemetry.send('didEnableResponsiveDimensions'); + } else { + strapi.telemetry.send('didDisableResponsiveDimensions'); + } + return strapi .store({ type: 'plugin', diff --git a/packages/strapi/lib/core/__tests__/fs.test.js b/packages/strapi/lib/core/__tests__/fs.test.js index c2718a5c1a..5051c7195a 100644 --- a/packages/strapi/lib/core/__tests__/fs.test.js +++ b/packages/strapi/lib/core/__tests__/fs.test.js @@ -1,5 +1,6 @@ const fs = require('../fs'); const fse = require('fs-extra'); +const path = require('path'); jest.mock('fs-extra'); @@ -23,8 +24,8 @@ describe('Strapi fs utils', () => { await strapiFS.writeAppFile('test', content); - expect(fse.ensureFile).toHaveBeenCalledWith('/tmp/test'); - expect(fse.writeFile).toHaveBeenCalledWith('/tmp/test', content); + expect(fse.ensureFile).toHaveBeenCalledWith(path.join('/', 'tmp', 'test')); + expect(fse.writeFile).toHaveBeenCalledWith(path.join('/', 'tmp', 'test'), content); }); test('Normalize the path to avoid relative access to folders in parent directories', async () => { @@ -34,8 +35,8 @@ describe('Strapi fs utils', () => { await strapiFS.writeAppFile('../../test', content); - expect(fse.ensureFile).toHaveBeenCalledWith('/tmp/test'); - expect(fse.writeFile).toHaveBeenCalledWith('/tmp/test', content); + expect(fse.ensureFile).toHaveBeenCalledWith(path.join('/', 'tmp', 'test')); + expect(fse.writeFile).toHaveBeenCalledWith(path.join('/', 'tmp', 'test'), content); }); test('Works with array path', async () => { @@ -45,8 +46,11 @@ describe('Strapi fs utils', () => { await strapiFS.writeAppFile(['test', 'sub', 'path'], content); - expect(fse.ensureFile).toHaveBeenCalledWith('/tmp/test/sub/path'); - expect(fse.writeFile).toHaveBeenCalledWith('/tmp/test/sub/path', content); + expect(fse.ensureFile).toHaveBeenCalledWith(path.join('/', 'tmp', 'test', 'sub', 'path')); + expect(fse.writeFile).toHaveBeenCalledWith( + path.join('/', 'tmp', 'test', 'sub', 'path'), + content + ); }); }); @@ -58,11 +62,7 @@ describe('Strapi fs utils', () => { strapiFS.writeAppFile = jest.fn(() => Promise.resolve()); - await strapiFS.writePluginFile( - 'users-permissions', - ['test', 'sub', 'path'], - content - ); + await strapiFS.writePluginFile('users-permissions', ['test', 'sub', 'path'], content); expect(strapiFS.writeAppFile).toHaveBeenCalledWith( 'extensions/users-permissions/test/sub/path', diff --git a/packages/strapi/lib/services/core-store.js b/packages/strapi/lib/services/core-store.js index 0b4b070573..88dee112b6 100644 --- a/packages/strapi/lib/services/core-store.js +++ b/packages/strapi/lib/services/core-store.js @@ -52,7 +52,7 @@ const createCoreStore = ({ environment: defaultEnv, db }) => { return null; } - if (data.type === 'object' || data.type === 'array' || data.type === 'boolean') { + if (data.type === 'object' || data.type === 'array' || data.type === 'boolean' || data.type === 'string') { try { return JSON.parse(data.value); } catch (err) { diff --git a/packages/strapi/lib/services/metrics/__tests__/rate-limiter.test.js b/packages/strapi/lib/services/metrics/__tests__/rate-limiter.test.js new file mode 100644 index 0000000000..b32bf2cad8 --- /dev/null +++ b/packages/strapi/lib/services/metrics/__tests__/rate-limiter.test.js @@ -0,0 +1,48 @@ +const wrapWithRateLimiter = require('../rate-limiter'); + +describe('Telemetry daily RateLimiter', () => { + test('Passes event and payload to sender', async () => { + const sender = jest.fn(() => Promise.resolve(true)); + + const send = wrapWithRateLimiter(sender, { limitedEvents: ['testEvent'] }); + + const payload = { key: 'value' }; + await send('notRestricted', payload); + + expect(sender).toHaveBeenCalledWith('notRestricted', payload); + }); + + test('Calls sender if event is not restricted', async () => { + const sender = jest.fn(() => Promise.resolve(true)); + + const send = wrapWithRateLimiter(sender, { limitedEvents: ['testEvent'] }); + + await send('notRestricted'); + + expect(sender).toHaveBeenCalledWith('notRestricted', undefined); + }); + + test('Calls the sender as many times as request when events is not restricted', async () => { + const sender = jest.fn(() => Promise.resolve(true)); + + const send = wrapWithRateLimiter(sender, { limitedEvents: ['testEvent'] }); + + await send('notRestricted'); + await send('notRestricted'); + await send('notRestricted'); + + expect(sender).toHaveBeenCalledTimes(3); + }); + + test('Calls the sender only once when event is restricted', async () => { + const sender = jest.fn(() => Promise.resolve(true)); + + const send = wrapWithRateLimiter(sender, { limitedEvents: ['restrictedEvent'] }); + + await send('restrictedEvent'); + await send('restrictedEvent'); + await send('restrictedEvent'); + + expect(sender).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/strapi/lib/services/metrics/index.js b/packages/strapi/lib/services/metrics/index.js index 9288e0b328..7dbd4b3be9 100644 --- a/packages/strapi/lib/services/metrics/index.js +++ b/packages/strapi/lib/services/metrics/index.js @@ -3,60 +3,27 @@ * Strapi telemetry package. * You can learn more at https://strapi.io/documentation/3.0.0-beta.x/global-strapi/usage-information.html#commitment-to-our-users-data-collection */ -const os = require('os'); -const isDocker = require('is-docker'); -const { machineIdSync } = require('node-machine-id'); -const fetch = require('node-fetch'); -const ciEnv = require('ci-info'); const { scheduleJob } = require('node-schedule'); +const wrapWithRateLimit = require('./rate-limiter'); +const createSender = require('./sender'); const createMiddleware = require('./middleware'); const isTruthy = require('./is-truthy'); +const LIMITED_EVENTS = [ + 'didSaveMediaWithAlternativeText', + 'didSaveMediaWithCaption', + 'didDisableResponsiveDimensions', + 'didEnableResponsiveDimensions', +]; + const createTelemetryInstance = strapi => { const uuid = strapi.config.uuid; - const deviceId = machineIdSync(); - const isDisabled = !uuid || isTruthy(process.env.STRAPI_TELEMETRY_DISABLED); - const anonymous_metadata = { - environment: strapi.config.environment, - os: os.type(), - osPlatform: os.platform(), - osRelease: os.release(), - nodeVersion: process.version, - docker: process.env.DOCKER || isDocker(), - isCI: ciEnv.isCI, - version: strapi.config.info.strapi, - strapiVersion: strapi.config.info.strapi, - }; - - const sendEvent = async (event, payload) => { - // do not send anything when user has disabled analytics - if (isDisabled) return true; - - try { - const res = await fetch('https://analytics.strapi.io/track', { - method: 'POST', - body: JSON.stringify({ - event, - uuid, - deviceId, - properties: { - ...payload, - ...anonymous_metadata, - }, - }), - timeout: 1000, - headers: { 'Content-Type': 'application/json' }, - }); - - return res.ok; - } catch (err) { - return false; - } - }; + const sender = createSender(strapi); + const sendEvent = wrapWithRateLimit(sender, { limitedEvents: LIMITED_EVENTS }); if (!isDisabled) { scheduleJob('0 0 12 * * *', () => sendEvent('ping')); @@ -64,7 +31,10 @@ const createTelemetryInstance = strapi => { } return { - send: sendEvent, + async send(event, payload) { + if (isDisabled) return true; + return sendEvent(event, payload); + }, }; }; diff --git a/packages/strapi/lib/services/metrics/rate-limiter.js b/packages/strapi/lib/services/metrics/rate-limiter.js new file mode 100644 index 0000000000..0b338b4ef8 --- /dev/null +++ b/packages/strapi/lib/services/metrics/rate-limiter.js @@ -0,0 +1,27 @@ +'use strict'; + +/** + * @param events a list of events that need to be limited + */ +module.exports = (sender, { limitedEvents = [] } = {}) => { + let currentDay = new Date().getDate(); + const eventCache = new Map(); + + return async (event, payload) => { + if (!limitedEvents.includes(event)) { + return sender(event, payload); + } + + if (new Date().getDate() !== currentDay) { + eventCache.clear(); + currentDay = new Date().getDate(); + } + + if (eventCache.has(event)) { + return false; + } + + eventCache.set(event, true); + return sender(event, payload); + }; +}; diff --git a/packages/strapi/lib/services/metrics/sender.js b/packages/strapi/lib/services/metrics/sender.js new file mode 100644 index 0000000000..bdd8b8f931 --- /dev/null +++ b/packages/strapi/lib/services/metrics/sender.js @@ -0,0 +1,53 @@ +'use strict'; + +const os = require('os'); + +const isDocker = require('is-docker'); +const { machineIdSync } = require('node-machine-id'); +const fetch = require('node-fetch'); +const ciEnv = require('ci-info'); + +/** + * Create a send function for event with all the necessary metadatas + * @param {Object} strapi strapi app + * @returns {Function} (event, payload) -> Promise{boolean} + */ +module.exports = strapi => { + const uuid = strapi.config.uuid; + const deviceId = machineIdSync(); + + const anonymous_metadata = { + environment: strapi.config.environment, + os: os.type(), + osPlatform: os.platform(), + osRelease: os.release(), + nodeVersion: process.version, + docker: process.env.DOCKER || isDocker(), + isCI: ciEnv.isCI, + version: strapi.config.info.strapi, + strapiVersion: strapi.config.info.strapi, + }; + + return async (event, payload = {}) => { + try { + const res = await fetch('https://analytics.strapi.io/track', { + method: 'POST', + body: JSON.stringify({ + event, + uuid, + deviceId, + properties: { + ...payload, + ...anonymous_metadata, + }, + }), + timeout: 1000, + headers: { 'Content-Type': 'application/json' }, + }); + + return res.ok; + } catch (err) { + return false; + } + }; +};