diff --git a/CHANGELOG.md b/CHANGELOG.md index c2b25ffb..ba624a4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [1.6.0] - 2020-03-06 + +### Added + +- Switch between daily, weekly and yearly views +- `ACKEE_ALLOW_ORIGIN` now supports multiple domains (#79 #82, thanks @jaryl) + +### Improved + +- JS error handling with React error boundary + +### Fixed + +- Loading indicator when the sizes-view is loading +- Catch errors when the sizes-view throws an error + ## [1.5.0] - 2020-02-16 ### Added diff --git a/docs/CORS headers.md b/docs/CORS headers.md index 7f1e785f..393ef8e8 100644 --- a/docs/CORS headers.md +++ b/docs/CORS headers.md @@ -51,10 +51,20 @@ Access-Control-Allow-Headers: Content-Type If you are running Ackee on a platform which handles SSL for you, you may want a quick solution for setting CORS headers instead of using a [reverse proxy](SSL%20and%20HTTPS.md). -As an environment variable, you will need to just set: +As an environment variable, you will need to set: -```bash +``` ACKEE_ALLOW_ORIGIN="https://example.com" ``` -The proper header value for `Access-Control-Allow-Origin` will be set with the other headers being the recommended values. \ No newline at end of file +The proper header value for `Access-Control-Allow-Origin` will be set with the other headers being the recommended values. + +It's also possible to allow requests from all domains (not recommended) or from multiple domains: + +``` +ACKEE_ALLOW_ORIGIN="*" +``` + +``` +ACKEE_ALLOW_ORIGIN="https://example.com,https://example2.com" +``` \ No newline at end of file diff --git a/docs/Options.md b/docs/Options.md index 7ecfc9d9..a175d84f 100644 --- a/docs/Options.md +++ b/docs/Options.md @@ -70,4 +70,24 @@ Set the environment to `development` to see additional details in the console an ``` NODE_ENV=development +``` + +## CORS headers + +Quick solution for setting [CORS headers](CORS%20headers.md) instead of using a [reverse proxy](SSL%20and%20HTTPS.md). This is helpful if you are running Ackee on a platform which handles SSL for you. + +``` +ACKEE_ALLOW_ORIGIN="*" +``` + +*or* + +``` +ACKEE_ALLOW_ORIGIN="https://example.com" +``` + +*or* + +``` +ACKEE_ALLOW_ORIGIN="https://example.com,https://example2.com" ``` \ No newline at end of file diff --git a/docs/sizes.md b/docs/sizes.md index c8f290e1..4d01e663 100644 --- a/docs/sizes.md +++ b/docs/sizes.md @@ -1,11 +1,11 @@ # Sizes -- [Get browser width](#get-browser-width) -- [Get browser height](#get-browser-height) -- [Get screen width](#get-screen-width) -- [Get screen height](#get-screen-height) +- [Get browser widths](#get-browser-widths) +- [Get browser heights](#get-browser-heights) +- [Get screen widths](#get-screen-widths) +- [Get screen heights](#get-screen-heights) -## Get browser width +## Get browser widths Get the top 25 browser widths of the last 7 days. @@ -42,7 +42,7 @@ Status: 200 OK } ``` -## Get browser height +## Get browser heights Get the top 25 browser heights of the last 7 days. @@ -79,7 +79,7 @@ Status: 200 OK } ``` -## Get screen width +## Get screen widths Get the top 25 screen widths of the last 7 days. @@ -116,7 +116,7 @@ Status: 200 OK } ``` -## Get screen height +## Get screen heights Get the top 25 screen heights of the last 7 days. diff --git a/docs/views.md b/docs/views.md index 0045fad5..6086f15e 100644 --- a/docs/views.md +++ b/docs/views.md @@ -5,12 +5,14 @@ ## Get unique site views -Get the unique amount of visits per day for the last 14 days. Days without entries are omitted. A user is unique when he visits a site for the first time a day. +Get the unique amount of visits per day, month or year for the last 14 intervals. Entries without views are omitted. A user is unique when he visits a site for the first time a day. ### Request ``` -GET /domains/:domainId/views?type=unique +GET /domains/:domainId/views?type=unique&interval=daily +GET /domains/:domainId/views?type=unique&interval=monthly +GET /domains/:domainId/views?type=unique&interval=yearly ``` ### Headers @@ -46,12 +48,14 @@ Status: 200 OK ## Get total page views -Get the total amount of visits per day for the last 14 days. Days without entries are omitted. +Get the total amount of visits per day, month or year for the last 14 intervals. Entries without views are omitted. ### Request ``` -GET /domains/:domainId/views?type=total +GET /domains/:domainId/views?type=total&interval=daily +GET /domains/:domainId/views?type=total&interval=monthly +GET /domains/:domainId/views?type=total&interval=yearly ``` ### Headers diff --git a/package.json b/package.json index ce1ef9db..28f314c1 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ackee", "private": true, - "version": "1.5.0", + "version": "1.6.0", "authors": [ "Tobias Reich " ], @@ -33,13 +33,14 @@ "@babel/preset-env": "^7.8.3", "ackee-tracker": "^3.2.2", "classnames": "^2.2.6", + "date-fns": "^2.9.0", "dotenv": "^8.2.0", "formbase": "^12.0.0", "immer": "^5.3.2", "is-url": "^1.2.4", "micro": "^9.3.4", "microrouter": "^3.1.3", - "mongoose": "^5.8.9", + "mongoose": "^5.9.1", "node-fetch": "^2.6.0", "node-schedule": "^1.3.2", "normalize-url": "^5.0.0", @@ -48,8 +49,9 @@ "prop-types": "^15.7.2", "react": "^16.12.0", "react-dom": "^16.12.0", - "react-redux": "^7.1.3", - "react-use": "^13.26.0", + "react-error-boundary": "^1.2.5", + "react-redux": "^7.2.0", + "react-use": "^13.26.1", "redux": "^4.0.5", "redux-devtools-extension": "^2.13.8", "redux-thunk": "^2.3.0", @@ -68,7 +70,7 @@ "eslint": "^6.8.0", "eslint-plugin-import": "^2.20.0", "eslint-plugin-react": "^7.17.0", - "eslint-plugin-react-hooks": "^2.3.0", + "eslint-plugin-react-hooks": "^2.4.0", "eslint-plugin-react-native": "^3.8.1", "mocked-env": "^1.3.2", "nyc": "^15.0.0", @@ -76,6 +78,7 @@ }, "ava": { "verbose": true, + "timeout": "20s", "environmentVariables": { "ACKEE_TRACKER": "custom name" } diff --git a/src/aggregations/aggregateDailyViews.js b/src/aggregations/aggregateDailyViews.js new file mode 100644 index 00000000..ffb6a031 --- /dev/null +++ b/src/aggregations/aggregateDailyViews.js @@ -0,0 +1,48 @@ +'use strict' + +module.exports = (id, unique) => { + + const aggregation = [ + { + $match: { + domainId: id + } + }, + { + $group: { + _id: { + day: { + $dayOfMonth: '$created' + }, + month: { + $month: '$created' + }, + year: { + $year: '$created' + } + }, + count: { + $sum: 1 + } + } + }, + { + $sort: { + '_id.year': -1, + '_id.month': -1, + '_id.day': -1 + } + }, + { + $limit: 14 + } + ] + + if (unique === true) aggregation[0].$match.clientId = { + $exists: true, + $ne: null + } + + return aggregation + +} \ No newline at end of file diff --git a/src/aggregations/aggregateMonthlyViews.js b/src/aggregations/aggregateMonthlyViews.js new file mode 100644 index 00000000..3fb33bfb --- /dev/null +++ b/src/aggregations/aggregateMonthlyViews.js @@ -0,0 +1,44 @@ +'use strict' + +module.exports = (id, unique) => { + + const aggregation = [ + { + $match: { + domainId: id + } + }, + { + $group: { + _id: { + month: { + $month: '$created' + }, + year: { + $year: '$created' + } + }, + count: { + $sum: 1 + } + } + }, + { + $sort: { + '_id.year': -1, + '_id.month': -1 + } + }, + { + $limit: 14 + } + ] + + if (unique === true) aggregation[0].$match.clientId = { + $exists: true, + $ne: null + } + + return aggregation + +} \ No newline at end of file diff --git a/src/utils/aggregateNewFields.js b/src/aggregations/aggregateNewFields.js similarity index 74% rename from src/utils/aggregateNewFields.js rename to src/aggregations/aggregateNewFields.js index a33f229a..13924bee 100644 --- a/src/utils/aggregateNewFields.js +++ b/src/aggregations/aggregateNewFields.js @@ -1,6 +1,8 @@ 'use strict' -const dateWithOffset = require('./dateWithOffset') +const { subDays } = require('date-fns') + +const zeroDate = require('../utils/zeroDate') module.exports = (id, property) => [ { @@ -25,7 +27,7 @@ module.exports = (id, property) => [ { $match: { created: { - $gte: dateWithOffset(-6) + $gte: subDays(zeroDate(), 6) } } }, diff --git a/src/utils/aggregateRecentFields.js b/src/aggregations/aggregateRecentFields.js similarity index 69% rename from src/utils/aggregateRecentFields.js rename to src/aggregations/aggregateRecentFields.js index f7dfc88e..dadcc6c3 100644 --- a/src/utils/aggregateRecentFields.js +++ b/src/aggregations/aggregateRecentFields.js @@ -1,6 +1,8 @@ 'use strict' -const dateWithOffset = require('./dateWithOffset') +const { subDays } = require('date-fns') + +const zeroDate = require('../utils/zeroDate') module.exports = (id, property) => [ { @@ -10,7 +12,7 @@ module.exports = (id, property) => [ $ne: null }, created: { - $gte: dateWithOffset(-6) + $gte: subDays(zeroDate(), 6) } } }, diff --git a/src/utils/aggregateTopFields.js b/src/aggregations/aggregateTopFields.js similarity index 70% rename from src/utils/aggregateTopFields.js rename to src/aggregations/aggregateTopFields.js index cd0df5b2..a2e7e67a 100644 --- a/src/utils/aggregateTopFields.js +++ b/src/aggregations/aggregateTopFields.js @@ -1,6 +1,8 @@ 'use strict' -const dateWithOffset = require('./dateWithOffset') +const { subDays } = require('date-fns') + +const zeroDate = require('../utils/zeroDate') module.exports = (id, property) => [ { @@ -10,7 +12,7 @@ module.exports = (id, property) => [ $ne: null }, created: { - $gte: dateWithOffset(-6) + $gte: subDays(zeroDate(), 6) } } }, diff --git a/src/aggregations/aggregateYearlyViews.js b/src/aggregations/aggregateYearlyViews.js new file mode 100644 index 00000000..10746e17 --- /dev/null +++ b/src/aggregations/aggregateYearlyViews.js @@ -0,0 +1,40 @@ +'use strict' + +module.exports = (id, unique) => { + + const aggregation = [ + { + $match: { + domainId: id + } + }, + { + $group: { + _id: { + year: { + $year: '$created' + } + }, + count: { + $sum: 1 + } + } + }, + { + $sort: { + '_id.day': -1 + } + }, + { + $limit: 14 + } + ] + + if (unique === true) aggregation[0].$match.clientId = { + $exists: true, + $ne: null + } + + return aggregation + +} \ No newline at end of file diff --git a/src/constants/views.js b/src/constants/views.js index 02a18ae1..42e7913c 100644 --- a/src/constants/views.js +++ b/src/constants/views.js @@ -2,8 +2,14 @@ // They will be used as values in the DOM and in the URL of the views calls. const VIEWS_TYPE_UNIQUE = 'unique' const VIEWS_TYPE_TOTAL = 'total' +const VIEWS_INTERVAL_DAILY = 'daily' +const VIEWS_INTERVAL_MONTHLY = 'monthly' +const VIEWS_INTERVAL_YEARLY = 'yearly' module.exports = { VIEWS_TYPE_UNIQUE, - VIEWS_TYPE_TOTAL + VIEWS_TYPE_TOTAL, + VIEWS_INTERVAL_DAILY, + VIEWS_INTERVAL_MONTHLY, + VIEWS_INTERVAL_YEARLY } \ No newline at end of file diff --git a/src/database/durations.js b/src/database/durations.js index 62a46aba..811d8cbb 100644 --- a/src/database/durations.js +++ b/src/database/durations.js @@ -1,14 +1,10 @@ 'use strict' -const Record = require('../schemas/Record') -const dateWithOffset = require('../utils/dateWithOffset') +const { subDays } = require('date-fns') -const { - DURATIONS_INTERVAL, - DURATIONS_LIMIT, - DURATIONS_TYPE_AVERAGE, - DURATIONS_TYPE_DETAILED -} = require('../constants/durations') +const Record = require('../schemas/Record') +const constants = require('../constants/durations') +const zeroDate = require('../utils/zeroDate') // The time that elapsed between the creation and updating of records. const projectDuration = { @@ -32,11 +28,11 @@ const projectInterval = { { $floor: [ { - $divide: [ '$duration', DURATIONS_INTERVAL ] + $divide: [ '$duration', constants.DURATIONS_INTERVAL ] } ] }, - DURATIONS_INTERVAL + constants.DURATIONS_INTERVAL ] } } @@ -52,9 +48,9 @@ const projectMinInterval = { duration: { $cond: { if: { - $lt: [ '$duration', DURATIONS_INTERVAL ] + $lt: [ '$duration', constants.DURATIONS_INTERVAL ] }, - then: DURATIONS_INTERVAL / 2, + then: constants.DURATIONS_INTERVAL / 2, else: '$duration' } } @@ -66,7 +62,7 @@ const projectMinInterval = { const matchLimit = { $match: { duration: { - $lt: DURATIONS_LIMIT + $lt: constants.DURATIONS_LIMIT } } } @@ -122,7 +118,7 @@ const getDetailed = async (id) => { $match: { domainId: id, created: { - $gte: dateWithOffset(-6) + $gte: subDays(zeroDate(), 6) } } }, @@ -148,7 +144,7 @@ const getDetailed = async (id) => { $match: { domainId: id, created: { - $gte: dateWithOffset(-6) + $gte: subDays(zeroDate(), 6) } } }, @@ -159,9 +155,9 @@ const getDetailed = async (id) => { _id: { $cond: { if: { - $gte: [ '$duration', DURATIONS_LIMIT ] + $gte: [ '$duration', constants.DURATIONS_LIMIT ] }, - then: DURATIONS_LIMIT, + then: constants.DURATIONS_LIMIT, else: '$duration' } }, @@ -189,8 +185,8 @@ const getDetailed = async (id) => { const get = async (id, type) => { switch (type) { - case DURATIONS_TYPE_AVERAGE: return getAverage(id) - case DURATIONS_TYPE_DETAILED: return getDetailed(id) + case constants.DURATIONS_TYPE_AVERAGE: return getAverage(id) + case constants.DURATIONS_TYPE_DETAILED: return getDetailed(id) } } diff --git a/src/database/languages.js b/src/database/languages.js index c7f0468f..ded919fc 100644 --- a/src/database/languages.js +++ b/src/database/languages.js @@ -1,13 +1,9 @@ 'use strict' const Record = require('../schemas/Record') -const aggregateTopFields = require('../utils/aggregateTopFields') -const aggregateRecentFields = require('../utils/aggregateRecentFields') - -const { - LANGUAGES_SORTING_TOP, - LANGUAGES_SORTING_RECENT -} = require('../constants/languages') +const aggregateTopFields = require('../aggregations/aggregateTopFields') +const aggregateRecentFields = require('../aggregations/aggregateRecentFields') +const constants = require('../constants/languages') const getTop = async (id) => { @@ -28,8 +24,8 @@ const getRecent = async (id) => { const get = async (id, sorting) => { switch (sorting) { - case LANGUAGES_SORTING_TOP: return getTop(id) - case LANGUAGES_SORTING_RECENT: return getRecent(id) + case constants.LANGUAGES_SORTING_TOP: return getTop(id) + case constants.LANGUAGES_SORTING_RECENT: return getRecent(id) } } diff --git a/src/database/pages.js b/src/database/pages.js index de88f416..b5200edf 100644 --- a/src/database/pages.js +++ b/src/database/pages.js @@ -1,13 +1,9 @@ 'use strict' const Record = require('../schemas/Record') -const aggregateTopFields = require('../utils/aggregateTopFields') -const aggregateRecentFields = require('../utils/aggregateRecentFields') - -const { - PAGES_SORTING_TOP, - PAGES_SORTING_RECENT -} = require('../constants/pages') +const aggregateTopFields = require('../aggregations/aggregateTopFields') +const aggregateRecentFields = require('../aggregations/aggregateRecentFields') +const constants = require('../constants/pages') const getTop = async (id) => { @@ -28,8 +24,8 @@ const getRecent = async (id) => { const get = async (id, sorting) => { switch (sorting) { - case PAGES_SORTING_TOP: return getTop(id) - case PAGES_SORTING_RECENT: return getRecent(id) + case constants.PAGES_SORTING_TOP: return getTop(id) + case constants.PAGES_SORTING_RECENT: return getRecent(id) } } diff --git a/src/database/referrers.js b/src/database/referrers.js index 6c7ac0a3..445eaf65 100644 --- a/src/database/referrers.js +++ b/src/database/referrers.js @@ -1,15 +1,10 @@ 'use strict' const Record = require('../schemas/Record') -const aggregateTopFields = require('../utils/aggregateTopFields') -const aggregateRecentFields = require('../utils/aggregateRecentFields') -const aggregateNewFields = require('../utils/aggregateNewFields') - -const { - REFERRERS_SORTING_TOP, - REFERRERS_SORTING_NEW, - REFERRERS_SORTING_RECENT -} = require('../constants/referrers') +const aggregateTopFields = require('../aggregations/aggregateTopFields') +const aggregateRecentFields = require('../aggregations/aggregateRecentFields') +const aggregateNewFields = require('../aggregations/aggregateNewFields') +const constants = require('../constants/referrers') const getTop = async (id) => { @@ -38,9 +33,9 @@ const getRecent = async (id) => { const get = async (id, sorting) => { switch (sorting) { - case REFERRERS_SORTING_TOP: return getTop(id) - case REFERRERS_SORTING_NEW: return getNew(id) - case REFERRERS_SORTING_RECENT: return getRecent(id) + case constants.REFERRERS_SORTING_TOP: return getTop(id) + case constants.REFERRERS_SORTING_NEW: return getNew(id) + case constants.REFERRERS_SORTING_RECENT: return getRecent(id) } } diff --git a/src/database/sizes.js b/src/database/sizes.js index 7581ae3f..4ee6cc73 100644 --- a/src/database/sizes.js +++ b/src/database/sizes.js @@ -1,14 +1,8 @@ 'use strict' const Record = require('../schemas/Record') -const aggregateTopFields = require('../utils/aggregateTopFields') - -const { - SIZES_TYPE_BROWSER_WIDTH, - SIZES_TYPE_BROWSER_HEIGHT, - SIZES_TYPE_SCREEN_WIDTH, - SIZES_TYPE_SCREEN_HEIGHT -} = require('../constants/sizes') +const aggregateTopFields = require('../aggregations/aggregateTopFields') +const constants = require('../constants/sizes') const getBrowserWidth = async (id) => { @@ -45,10 +39,10 @@ const getScreenHeight = async (id) => { const get = async (id, type) => { switch (type) { - case SIZES_TYPE_BROWSER_WIDTH: return getBrowserWidth(id) - case SIZES_TYPE_BROWSER_HEIGHT: return getBrowserHeight(id) - case SIZES_TYPE_SCREEN_WIDTH: return getScreenWidth(id) - case SIZES_TYPE_SCREEN_HEIGHT: return getScreenHeight(id) + case constants.SIZES_TYPE_BROWSER_WIDTH: return getBrowserWidth(id) + case constants.SIZES_TYPE_BROWSER_HEIGHT: return getBrowserHeight(id) + case constants.SIZES_TYPE_SCREEN_WIDTH: return getScreenWidth(id) + case constants.SIZES_TYPE_SCREEN_HEIGHT: return getScreenHeight(id) } } diff --git a/src/database/views.js b/src/database/views.js index 272d6eac..f98524ce 100644 --- a/src/database/views.js +++ b/src/database/views.js @@ -1,101 +1,48 @@ 'use strict' const Record = require('../schemas/Record') - -const { - VIEWS_TYPE_UNIQUE, - VIEWS_TYPE_TOTAL -} = require('../constants/views') - -const getUnique = async (id) => { - - return Record.aggregate([ - { - $match: { - clientId: { - $exists: true, - $ne: null - }, - domainId: id - } - }, - { - $group: { - _id: { - day: { - $dayOfMonth: '$created' - }, - month: { - $month: '$created' - }, - year: { - $year: '$created' - } - }, - count: { - $sum: 1 - } - } - }, - { - $sort: { - '_id.year': -1, - '_id.month': -1, - '_id.day': -1 - } - }, - { - $limit: 14 - } - ]) +const aggregateDailyViews = require('../aggregations/aggregateDailyViews') +const aggregateMonthlyViews = require('../aggregations/aggregateMonthlyViews') +const aggregateYearlyViews = require('../aggregations/aggregateYearlyViews') +const constants = require('../constants/views') + +const getUnique = async (id, interval) => { + + switch (interval) { + case constants.VIEWS_INTERVAL_DAILY: return Record.aggregate( + aggregateDailyViews(id, true) + ) + case constants.VIEWS_INTERVAL_MONTHLY: return Record.aggregate( + aggregateMonthlyViews(id, true) + ) + case constants.VIEWS_INTERVAL_YEARLY: return Record.aggregate( + aggregateYearlyViews(id, true) + ) + } } -const getTotal = async (id) => { - - return Record.aggregate([ - { - $match: { - domainId: id - } - }, - { - $group: { - _id: { - day: { - $dayOfMonth: '$created' - }, - month: { - $month: '$created' - }, - year: { - $year: '$created' - } - }, - count: { - $sum: 1 - } - } - }, - { - $sort: { - '_id.year': -1, - '_id.month': -1, - '_id.day': -1 - } - }, - { - $limit: 14 - } - ]) +const getTotal = async (id, interval) => { + + switch (interval) { + case constants.VIEWS_INTERVAL_DAILY: return Record.aggregate( + aggregateDailyViews(id, false) + ) + case constants.VIEWS_INTERVAL_MONTHLY: return Record.aggregate( + aggregateMonthlyViews(id, false) + ) + case constants.VIEWS_INTERVAL_YEARLY: return Record.aggregate( + aggregateYearlyViews(id, false) + ) + } } -const get = async (id, type) => { +const get = async (id, type, interval) => { switch (type) { - case VIEWS_TYPE_UNIQUE: return getUnique(id) - case VIEWS_TYPE_TOTAL: return getTotal(id) + case constants.VIEWS_TYPE_UNIQUE: return getUnique(id, interval) + case constants.VIEWS_TYPE_TOTAL: return getTotal(id, interval) } } diff --git a/src/routes/durations.js b/src/routes/durations.js index 07c95f18..4ccdbeb4 100644 --- a/src/routes/durations.js +++ b/src/routes/durations.js @@ -3,11 +3,7 @@ const { createError } = require('micro') const durations = require('../database/durations') - -const { - DURATIONS_TYPE_AVERAGE, - DURATIONS_TYPE_DETAILED -} = require('../constants/durations') +const constants = require('../constants/durations') const response = (entry) => ({ type: 'duration', @@ -28,13 +24,16 @@ const get = async (req) => { const { domainId } = req.params const { type } = req.query + const types = [ + constants.DURATIONS_TYPE_AVERAGE, + constants.DURATIONS_TYPE_DETAILED + ] + + if (types.includes(type) === false) throw createError(400, 'Unknown type') + const entries = await durations.get(domainId, type) - switch (type) { - case DURATIONS_TYPE_AVERAGE: return responses(entries) - case DURATIONS_TYPE_DETAILED: return responses(entries) - default: throw createError(400, 'Unknown type') - } + return responses(entries) } diff --git a/src/routes/languages.js b/src/routes/languages.js index 2cafabdb..fc63c646 100644 --- a/src/routes/languages.js +++ b/src/routes/languages.js @@ -3,11 +3,7 @@ const { createError } = require('micro') const languages = require('../database/languages') - -const { - LANGUAGES_SORTING_TOP, - LANGUAGES_SORTING_RECENT -} = require('../constants/languages') +const constants = require('../constants/languages') const response = (entry) => ({ type: 'language', @@ -28,13 +24,16 @@ const get = async (req) => { const { domainId } = req.params const { sorting } = req.query + const sortings = [ + constants.LANGUAGES_SORTING_TOP, + constants.LANGUAGES_SORTING_RECENT + ] + + if (sortings.includes(sorting) === false) throw createError(400, 'Unknown sorting') + const entries = await languages.get(domainId, sorting) - switch (sorting) { - case LANGUAGES_SORTING_TOP: return responses(entries) - case LANGUAGES_SORTING_RECENT: return responses(entries) - default: throw createError(400, 'Unknown sorting') - } + return responses(entries) } diff --git a/src/routes/pages.js b/src/routes/pages.js index c43f0b30..a2f7e580 100644 --- a/src/routes/pages.js +++ b/src/routes/pages.js @@ -3,11 +3,7 @@ const { createError } = require('micro') const pages = require('../database/pages') - -const { - PAGES_SORTING_TOP, - PAGES_SORTING_RECENT -} = require('../constants/pages') +const constants = require('../constants/pages') const response = (entry) => ({ type: 'page', @@ -28,13 +24,16 @@ const get = async (req) => { const { domainId } = req.params const { sorting } = req.query + const sortings = [ + constants.PAGES_SORTING_TOP, + constants.PAGES_SORTING_RECENT + ] + + if (sortings.includes(sorting) === false) throw createError(400, 'Unknown sorting') + const entries = await pages.get(domainId, sorting) - switch (sorting) { - case PAGES_SORTING_TOP: return responses(entries) - case PAGES_SORTING_RECENT: return responses(entries) - default: throw createError(400, 'Unknown sorting') - } + return responses(entries) } diff --git a/src/routes/referrers.js b/src/routes/referrers.js index 3ae7e6db..a4f07dfb 100644 --- a/src/routes/referrers.js +++ b/src/routes/referrers.js @@ -3,12 +3,7 @@ const { createError } = require('micro') const referrers = require('../database/referrers') - -const { - REFERRERS_SORTING_TOP, - REFERRERS_SORTING_NEW, - REFERRERS_SORTING_RECENT -} = require('../constants/referrers') +const constants = require('../constants/referrers') const response = (entry) => ({ type: 'referrer', @@ -29,14 +24,17 @@ const get = async (req) => { const { domainId } = req.params const { sorting } = req.query + const sortings = [ + constants.REFERRERS_SORTING_TOP, + constants.REFERRERS_SORTING_NEW, + constants.REFERRERS_SORTING_RECENT + ] + + if (sortings.includes(sorting) === false) throw createError(400, 'Unknown sorting') + const entries = await referrers.get(domainId, sorting) - switch (sorting) { - case REFERRERS_SORTING_TOP: return responses(entries) - case REFERRERS_SORTING_NEW: return responses(entries) - case REFERRERS_SORTING_RECENT: return responses(entries) - default: throw createError(400, 'Unknown sorting') - } + return responses(entries) } diff --git a/src/routes/sizes.js b/src/routes/sizes.js index 4d8223d7..33395152 100644 --- a/src/routes/sizes.js +++ b/src/routes/sizes.js @@ -3,13 +3,7 @@ const { createError } = require('micro') const sizes = require('../database/sizes') - -const { - SIZES_TYPE_BROWSER_WIDTH, - SIZES_TYPE_BROWSER_HEIGHT, - SIZES_TYPE_SCREEN_WIDTH, - SIZES_TYPE_SCREEN_HEIGHT -} = require('../constants/sizes') +const constants = require('../constants/sizes') const response = (entry) => ({ type: 'size', @@ -30,15 +24,18 @@ const get = async (req) => { const { domainId } = req.params const { type } = req.query + const types = [ + constants.SIZES_TYPE_BROWSER_WIDTH, + constants.SIZES_TYPE_BROWSER_HEIGHT, + constants.SIZES_TYPE_SCREEN_WIDTH, + constants.SIZES_TYPE_SCREEN_HEIGHT + ] + + if (types.includes(type) === false) throw createError(400, 'Unknown type') + const entries = await sizes.get(domainId, type) - switch (type) { - case SIZES_TYPE_BROWSER_WIDTH: return responses(entries) - case SIZES_TYPE_BROWSER_HEIGHT: return responses(entries) - case SIZES_TYPE_SCREEN_WIDTH: return responses(entries) - case SIZES_TYPE_SCREEN_HEIGHT: return responses(entries) - default: throw createError(400, 'Unknown type') - } + return responses(entries) } diff --git a/src/routes/views.js b/src/routes/views.js index 136c23b0..5ba773e1 100644 --- a/src/routes/views.js +++ b/src/routes/views.js @@ -3,11 +3,7 @@ const { createError } = require('micro') const views = require('../database/views') - -const { - VIEWS_TYPE_UNIQUE, - VIEWS_TYPE_TOTAL -} = require('../constants/views') +const constants = require('../constants/views') const response = (entry) => ({ type: 'view', @@ -29,15 +25,25 @@ const responses = (entries) => ({ const get = async (req) => { const { domainId } = req.params - const { type } = req.query + const { type, interval } = req.query - const entries = await views.get(domainId, type) + const types = [ + constants.VIEWS_TYPE_UNIQUE, + constants.VIEWS_TYPE_TOTAL + ] - switch (type) { - case VIEWS_TYPE_UNIQUE: return responses(entries) - case VIEWS_TYPE_TOTAL: return responses(entries) - default: throw createError(400, 'Unknown type') - } + const intervals = [ + constants.VIEWS_INTERVAL_DAILY, + constants.VIEWS_INTERVAL_MONTHLY, + constants.VIEWS_INTERVAL_YEARLY + ] + + if (types.includes(type) === false) throw createError(400, 'Unknown type') + if (intervals.includes(interval) === false) throw createError(400, 'Unknown interval') + + const entries = await views.get(domainId, type, interval) + + return responses(entries) } diff --git a/src/server.js b/src/server.js index 256f22bc..2e5eb5ab 100644 --- a/src/server.js +++ b/src/server.js @@ -47,7 +47,16 @@ const catchError = (fn) => async (req, res) => { const attachCorsHeaders = (fn) => async (req, res) => { - const allowOrigin = process.env.ACKEE_ALLOW_ORIGIN + const allowOrigin = (() => { + + if (process.env.ACKEE_ALLOW_ORIGIN === '*') return '*' + + if (process.env.ACKEE_ALLOW_ORIGIN) { + const origins = process.env.ACKEE_ALLOW_ORIGIN.split(',') + return origins.find((origin) => origin.includes(req.headers.host)) + } + + })() if (allowOrigin != null) { res.setHeader('Access-Control-Allow-Origin', allowOrigin) @@ -114,4 +123,4 @@ module.exports = micro( router(...routes) ) ) -) \ No newline at end of file +) diff --git a/src/ui/scripts/actions/views.js b/src/ui/scripts/actions/views.js index 19251544..0ec3080f 100644 --- a/src/ui/scripts/actions/views.js +++ b/src/ui/scripts/actions/views.js @@ -2,6 +2,7 @@ import api from '../utils/api' import signalHandler from '../utils/signalHandler' export const SET_VIEWS_TYPE = Symbol() +export const SET_VIEWS_INTERVAL = Symbol() export const SET_VIEWS_VALUE = Symbol() export const SET_VIEWS_FETCHING = Symbol() export const SET_VIEWS_ERROR = Symbol() @@ -12,6 +13,11 @@ export const setViewsType = (payload) => ({ payload }) +export const setViewsInterval = (payload) => ({ + type: SET_VIEWS_INTERVAL, + payload +}) + export const setViewsValue = (domainId, payload) => ({ type: SET_VIEWS_VALUE, domainId, @@ -41,7 +47,7 @@ export const fetchViews = signalHandler((signal) => (props, domainId) => async ( try { - const data = await api(`/domains/${ domainId }/views?type=${ props.views.type }`, { + const data = await api(`/domains/${ domainId }/views?type=${ props.views.type }&interval=${ props.views.interval }`, { method: 'get', props, signal: signal(domainId) diff --git a/src/ui/scripts/components/ErrorFallback.js b/src/ui/scripts/components/ErrorFallback.js new file mode 100644 index 00000000..41dd2612 --- /dev/null +++ b/src/ui/scripts/components/ErrorFallback.js @@ -0,0 +1,18 @@ +import { createElement as h } from 'react' +import PropTypes from 'prop-types' + +import OverlayFailure from './overlays/OverlayFailure' + +const ErrorFallback = (props) => { + + return h(OverlayFailure, { + errors: [ props.error ] + }) + +} + +ErrorFallback.propTypes = { + error: PropTypes.any.isRequired +} + +export default ErrorFallback \ No newline at end of file diff --git a/src/ui/scripts/components/Main.js b/src/ui/scripts/components/Main.js index 54126ae6..2f8739c4 100644 --- a/src/ui/scripts/components/Main.js +++ b/src/ui/scripts/components/Main.js @@ -1,9 +1,11 @@ import { createElement as h } from 'react' +import { withErrorBoundary } from 'react-error-boundary' import isUnknownError from '../utils/isUnknownError' import OverlayFailure from './overlays/OverlayFailure' import OverlayLogin from './overlays/OverlayLogin' +import ErrorFallback from './ErrorFallback' import Dashboard from './Dashboard' const Main = (props) => { @@ -24,4 +26,4 @@ const Main = (props) => { } -export default Main \ No newline at end of file +export default withErrorBoundary(Main, ErrorFallback) \ No newline at end of file diff --git a/src/ui/scripts/components/cards/CardViews.js b/src/ui/scripts/components/cards/CardViews.js index 2312ea83..a5eb066a 100644 --- a/src/ui/scripts/components/cards/CardViews.js +++ b/src/ui/scripts/components/cards/CardViews.js @@ -2,12 +2,28 @@ import { createElement as h, useState } from 'react' import PropTypes from 'prop-types' import classNames from 'classnames' +import { + VIEWS_INTERVAL_DAILY, + VIEWS_INTERVAL_MONTHLY, + VIEWS_INTERVAL_YEARLY +} from '../../../../constants/views' + import relativeDays from '../../utils/relativeDays' +import relativeMonths from '../../utils/relativeMonths' +import relativeYears from '../../utils/relativeYears' import Headline from '../Headline' import Text from '../Text' import PresentationBarChart from '../presentations/PresentationBarChart' +const relativeFn = (interval) => { + switch (interval) { + case VIEWS_INTERVAL_DAILY: return relativeDays + case VIEWS_INTERVAL_MONTHLY: return relativeMonths + case VIEWS_INTERVAL_YEARLY: return relativeYears + } +} + const CardViews = (props) => { // Index of the active element @@ -31,7 +47,7 @@ const CardViews = (props) => { }, props.headline), h(Text, { spacing: false - }, relativeDays(active)), + }, relativeFn(props.interval)(active)), h(PresentationBarChart, { items: props.items, active: active, @@ -47,6 +63,7 @@ const CardViews = (props) => { CardViews.propTypes = { wide: PropTypes.bool, headline: PropTypes.string.isRequired, + interval: PropTypes.string.isRequired, items: PropTypes.array.isRequired } diff --git a/src/ui/scripts/components/routes/RouteViews.js b/src/ui/scripts/components/routes/RouteViews.js index f676604d..7181b8e5 100644 --- a/src/ui/scripts/components/routes/RouteViews.js +++ b/src/ui/scripts/components/routes/RouteViews.js @@ -2,7 +2,10 @@ import { createElement as h, Fragment, useEffect } from 'react' import { VIEWS_TYPE_UNIQUE, - VIEWS_TYPE_TOTAL + VIEWS_TYPE_TOTAL, + VIEWS_INTERVAL_DAILY, + VIEWS_INTERVAL_MONTHLY, + VIEWS_INTERVAL_YEARLY } from '../../../../constants/views' import enhanceViews from '../../enhancers/enhanceViews' @@ -26,7 +29,7 @@ const RouteViews = (props) => { props.fetchViews(props, domain.data.id) }) - }, [ props.domains.value, props.views.type ]) + }, [ props.domains.value, props.views.type, props.views.interval ]) return ( h(Fragment, {}, @@ -39,6 +42,15 @@ const RouteViews = (props) => { { value: VIEWS_TYPE_UNIQUE, label: 'Unique site views' }, { value: VIEWS_TYPE_TOTAL, label: 'Total page views' } ] + }), + h(Select, { + value: props.views.interval, + onChange: (e) => props.setViewsInterval(e.target.value), + items: [ + { value: VIEWS_INTERVAL_DAILY, label: 'Daily' }, + { value: VIEWS_INTERVAL_MONTHLY, label: 'Monthly' }, + { value: VIEWS_INTERVAL_YEARLY, label: 'Yearly' } + ] }) ), @@ -48,6 +60,7 @@ const RouteViews = (props) => { [VIEWS_TYPE_UNIQUE]: 'Site Views', [VIEWS_TYPE_TOTAL]: 'Page Views' })[props.views.type], + interval: props.views.interval, items: mergeViews(props.domains, props.views) }), @@ -56,7 +69,8 @@ const RouteViews = (props) => { h(CardViews, { key: domain.data.id, headline: domain.data.title, - items: props.views.value[domain.data.id] == null ? [] : enhanceViews(props.views.value[domain.data.id].value, 7) + interval: props.views.interval, + items: props.views.value[domain.data.id] == null ? [] : enhanceViews(props.views.value[domain.data.id].value, 7, props.views.interval) }) ) ) diff --git a/src/ui/scripts/enhancers/enhanceAverageDurations.js b/src/ui/scripts/enhancers/enhanceAverageDurations.js index 908bb73e..0a4a9aa5 100644 --- a/src/ui/scripts/enhancers/enhanceAverageDurations.js +++ b/src/ui/scripts/enhancers/enhanceAverageDurations.js @@ -1,10 +1,11 @@ -import dateWithOffset from '../../../utils/dateWithOffset' +import { subDays } from 'date-fns' + import createArray from '../utils/createArray' import matchesDate from '../utils/matchesDate' export default (durations, length) => createArray(length).map((_, index) => { - const date = dateWithOffset(index * -1) + const date = subDays(new Date(), index) // Find a duration that matches the date const duration = durations.find((duration) => { diff --git a/src/ui/scripts/enhancers/enhanceState.js b/src/ui/scripts/enhancers/enhanceState.js index 27b0620c..19b681d5 100644 --- a/src/ui/scripts/enhancers/enhanceState.js +++ b/src/ui/scripts/enhancers/enhanceState.js @@ -8,6 +8,7 @@ export default (state) => { Object.values(state.referrers.value).some((value) => value.fetching) === true || Object.values(state.durations.value).some((value) => value.fetching) === true || Object.values(state.languages.value).some((value) => value.fetching) === true || + Object.values(state.sizes.value).some((value) => value.fetching) === true || state.domains.fetching === true || state.token.fetching === true ) @@ -18,6 +19,7 @@ export default (state) => { ...Object.values(state.referrers.value).map((value) => value.error), ...Object.values(state.durations.value).map((value) => value.error), ...Object.values(state.languages.value).map((value) => value.error), + ...Object.values(state.sizes.value).map((value) => value.error), state.domains.error, state.token.error ].filter(isDefined) diff --git a/src/ui/scripts/enhancers/enhanceViews.js b/src/ui/scripts/enhancers/enhanceViews.js index 80894add..80f18d20 100644 --- a/src/ui/scripts/enhancers/enhanceViews.js +++ b/src/ui/scripts/enhancers/enhanceViews.js @@ -1,14 +1,38 @@ -import dateWithOffset from '../../../utils/dateWithOffset' +import { subDays, subMonths, subYears } from 'date-fns' + +import { + VIEWS_INTERVAL_DAILY, + VIEWS_INTERVAL_MONTHLY, + VIEWS_INTERVAL_YEARLY +} from '../../../constants/views' + import createArray from '../utils/createArray' import matchesDate from '../utils/matchesDate' -export default (views, length) => createArray(length).map((_, index) => { +const subFn = (interval) => { + switch (interval) { + case VIEWS_INTERVAL_DAILY: return subDays + case VIEWS_INTERVAL_MONTHLY: return subMonths + case VIEWS_INTERVAL_YEARLY: return subYears + } +} + +export default (views, length, interval) => createArray(length).map((_, index) => { + + const matchDay = [ VIEWS_INTERVAL_DAILY ].includes(interval) + const matchMonth = [ VIEWS_INTERVAL_DAILY, VIEWS_INTERVAL_MONTHLY ].includes(interval) + const matchYear = [ VIEWS_INTERVAL_DAILY, VIEWS_INTERVAL_MONTHLY, VIEWS_INTERVAL_YEARLY ].includes(interval) - const date = dateWithOffset(index * -1) + const date = subFn(interval)(new Date(), index) // Find a view that matches the date const view = views.find((view) => { - return matchesDate(view.data.id.day, view.data.id.month, view.data.id.year, date) + return matchesDate( + matchDay === true ? view.data.id.day : undefined, + matchMonth === true ? view.data.id.month : undefined, + matchYear === true ? view.data.id.year : undefined, + date + ) }) return view == null ? 0 : view.data.count diff --git a/src/ui/scripts/index.js b/src/ui/scripts/index.js index 50bf75b0..46813b1b 100644 --- a/src/ui/scripts/index.js +++ b/src/ui/scripts/index.js @@ -50,7 +50,8 @@ store.subscribe(() => { }, views: { ...initialViewsState(), - type: currentState.views.type + type: currentState.views.type, + interval: currentState.views.interval }, pages: { ...initialPagesState(), diff --git a/src/ui/scripts/reducers/views.js b/src/ui/scripts/reducers/views.js index 8add6453..db0d364d 100644 --- a/src/ui/scripts/reducers/views.js +++ b/src/ui/scripts/reducers/views.js @@ -2,6 +2,7 @@ import produce from 'immer' import { SET_VIEWS_TYPE, + SET_VIEWS_INTERVAL, SET_VIEWS_VALUE, SET_VIEWS_FETCHING, SET_VIEWS_ERROR, @@ -9,11 +10,13 @@ import { } from '../actions' import { - VIEWS_TYPE_UNIQUE + VIEWS_TYPE_UNIQUE, + VIEWS_INTERVAL_DAILY } from '../../../constants/views' export const initialState = () => ({ type: VIEWS_TYPE_UNIQUE, + interval: VIEWS_INTERVAL_DAILY, value: {} }) @@ -34,6 +37,9 @@ export default produce((draft, action) => { case SET_VIEWS_TYPE: draft.type = action.payload || initialState().type break + case SET_VIEWS_INTERVAL: + draft.interval = action.payload || initialState().interval + break case SET_VIEWS_VALUE: draft.value[action.domainId].value = action.payload || initialSubState().value break diff --git a/src/ui/scripts/utils/matchesDate.js b/src/ui/scripts/utils/matchesDate.js index 57acd6ee..c5a8945f 100644 --- a/src/ui/scripts/utils/matchesDate.js +++ b/src/ui/scripts/utils/matchesDate.js @@ -1,8 +1,8 @@ export default (day, month, year, date) => { - const isDay = day === date.getDate() - const isMonth = month === date.getMonth() + 1 - const isYear = year === date.getFullYear() + const isDay = day === date.getDate() || day == null + const isMonth = month === date.getMonth() + 1 || month == null + const isYear = year === date.getFullYear() || year == null return isDay === true && isMonth === true && isYear === true diff --git a/src/ui/scripts/utils/mergeViews.js b/src/ui/scripts/utils/mergeViews.js index b96d29e9..bde69df1 100644 --- a/src/ui/scripts/utils/mergeViews.js +++ b/src/ui/scripts/utils/mergeViews.js @@ -10,7 +10,7 @@ export default (domains, views) => { const view = views.value[domain.data.id] const exists = view != null - return exists === true ? enhanceViews(view.value, 14) : undefined + return exists === true ? enhanceViews(view.value, 14, views.interval) : undefined }) @@ -20,13 +20,13 @@ export default (domains, views) => { // Merge all views to one array of views return filteredViews.reduce((acc, views) => { - // Views is an array. Each item represents the visit count of one day. + // Views is an array. Each item represents the visit count of one day, month or year. views.forEach((view, index) => { - // The current day might be new as should be initialised first + // The current day, month or year might be new and should be initialised first const initial = acc[index] == null ? 0 : acc[index] - // Add the current day to the global array of days + // Add the current day, month or year to the global array of days, months or years acc[index] = initial + view }) diff --git a/src/ui/scripts/utils/relativeMonths.js b/src/ui/scripts/utils/relativeMonths.js new file mode 100644 index 00000000..d86faabc --- /dev/null +++ b/src/ui/scripts/utils/relativeMonths.js @@ -0,0 +1,11 @@ +import { subMonths } from 'date-fns' + +export default (offset) => { + + switch (offset) { + case 0: return 'This month' + case 1: return 'Last month' + default: return subMonths(new Date(), offset).toLocaleString('en-US', { month: 'long', year: 'numeric' }) + } + +} \ No newline at end of file diff --git a/src/ui/scripts/utils/relativeYears.js b/src/ui/scripts/utils/relativeYears.js new file mode 100644 index 00000000..27b43dd9 --- /dev/null +++ b/src/ui/scripts/utils/relativeYears.js @@ -0,0 +1,11 @@ +import { subYears } from 'date-fns' + +export default (offset) => { + + switch (offset) { + case 0: return 'This year' + case 1: return 'Last year' + default: return subYears(new Date(), offset).getFullYear() + } + +} \ No newline at end of file diff --git a/src/ui/styles/_barChart.scss b/src/ui/styles/_barChart.scss index 4f52a4b1..3214b22e 100644 --- a/src/ui/styles/_barChart.scss +++ b/src/ui/styles/_barChart.scss @@ -4,7 +4,7 @@ display: grid; grid-auto-flow: column; grid-template-columns: min-content auto; - grid-gap: $gutter/2; + gap: $gutter/2; padding-top: $gutter*3.5; height: $cardContentHeight; overflow: hidden; diff --git a/src/ui/styles/_content.scss b/src/ui/styles/_content.scss index 311cfe19..f8475d15 100644 --- a/src/ui/styles/_content.scss +++ b/src/ui/styles/_content.scss @@ -2,7 +2,7 @@ display: grid; grid-template-columns: 1fr 1fr; - grid-gap: $gutter; + gap: $gutter; margin: 0 auto; padding: $gutter*5 $gutter; width: 100%; diff --git a/src/ui/styles/_emptyState.scss b/src/ui/styles/_emptyState.scss index d902e5a0..fc4e8ad8 100644 --- a/src/ui/styles/_emptyState.scss +++ b/src/ui/styles/_emptyState.scss @@ -8,8 +8,8 @@ &__inner { display: grid; align-items: center; - grid-gap: $gutter/1.5; grid-auto-flow: column; + gap: $gutter/1.5; } &__icon { diff --git a/src/ui/styles/_subHeader.scss b/src/ui/styles/_subHeader.scss index 23547edc..b918fdff 100644 --- a/src/ui/styles/_subHeader.scss +++ b/src/ui/styles/_subHeader.scss @@ -2,8 +2,9 @@ grid-column: 1 / -1; display: grid; - justify-content: flex-end; grid-auto-flow: column; grid-template-columns: max-content; + justify-content: flex-end; + gap: $gutter/2; } \ No newline at end of file diff --git a/src/ui/styles/_valuesBar.scss b/src/ui/styles/_valuesBar.scss index 83270d0d..1909cbf8 100644 --- a/src/ui/styles/_valuesBar.scss +++ b/src/ui/styles/_valuesBar.scss @@ -2,14 +2,14 @@ display: grid; grid-template-rows: auto 1em; - grid-gap: $gutter/2; + gap: $gutter/2; padding-top: $gutter*1.5; height: $cardContentHeight; &__row { display: grid; grid-template-columns: 3em auto; - grid-gap: $gutter/2; + gap: $gutter/2; } &__bar { diff --git a/src/utils/dateWithOffset.js b/src/utils/dateWithOffset.js deleted file mode 100644 index 246cc720..00000000 --- a/src/utils/dateWithOffset.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict' - -const { day } = require('./times') - -module.exports = (offset) => { - - const currentDate = new Date() - - currentDate.setHours(0) - currentDate.setMinutes(0) - currentDate.setSeconds(0) - currentDate.setMilliseconds(0) - - currentDate.setTime(currentDate.getTime() + day * offset) - - return currentDate - -} \ No newline at end of file diff --git a/src/utils/zeroDate.js b/src/utils/zeroDate.js new file mode 100644 index 00000000..5c02daae --- /dev/null +++ b/src/utils/zeroDate.js @@ -0,0 +1,14 @@ +'use strict' + +module.exports = () => { + + const date = new Date() + + date.setHours(0) + date.setMinutes(0) + date.setSeconds(0) + date.setMilliseconds(0) + + return date + +} \ No newline at end of file diff --git a/test/aggregations/aggregateDailyViews.js b/test/aggregations/aggregateDailyViews.js new file mode 100644 index 00000000..a26bda0e --- /dev/null +++ b/test/aggregations/aggregateDailyViews.js @@ -0,0 +1,22 @@ +'use strict' + +const test = require('ava') +const uuid = require('uuid/v4') + +const aggregateDailyViews = require('../../src/aggregations/aggregateDailyViews') + +test('return unique aggregation', async (t) => { + + const result = aggregateDailyViews(uuid(), true) + + t.true(Array.isArray(result)) + +}) + +test('return non-unique aggregation', async (t) => { + + const result = aggregateDailyViews(uuid(), false) + + t.true(Array.isArray(result)) + +}) \ No newline at end of file diff --git a/test/aggregations/aggregateMonthlyViews.js b/test/aggregations/aggregateMonthlyViews.js new file mode 100644 index 00000000..66f45b36 --- /dev/null +++ b/test/aggregations/aggregateMonthlyViews.js @@ -0,0 +1,22 @@ +'use strict' + +const test = require('ava') +const uuid = require('uuid/v4') + +const aggregateMonthlyViews = require('../../src/aggregations/aggregateMonthlyViews') + +test('return unique aggregation', async (t) => { + + const result = aggregateMonthlyViews(uuid(), true) + + t.true(Array.isArray(result)) + +}) + +test('return non-unique aggregation', async (t) => { + + const result = aggregateMonthlyViews(uuid(), false) + + t.true(Array.isArray(result)) + +}) \ No newline at end of file diff --git a/test/aggregations/aggregateNewFields.js b/test/aggregations/aggregateNewFields.js new file mode 100644 index 00000000..5dbcba3b --- /dev/null +++ b/test/aggregations/aggregateNewFields.js @@ -0,0 +1,14 @@ +'use strict' + +const test = require('ava') +const uuid = require('uuid/v4') + +const aggregateNewFields = require('../../src/aggregations/aggregateNewFields') + +test('return array', async (t) => { + + const result = aggregateNewFields(uuid(), 'siteReferrer') + + t.true(Array.isArray(result)) + +}) \ No newline at end of file diff --git a/test/aggregations/aggregateRecentFields.js b/test/aggregations/aggregateRecentFields.js new file mode 100644 index 00000000..dab4b9b6 --- /dev/null +++ b/test/aggregations/aggregateRecentFields.js @@ -0,0 +1,14 @@ +'use strict' + +const test = require('ava') +const uuid = require('uuid/v4') + +const aggregateRecentFields = require('../../src/aggregations/aggregateRecentFields') + +test('return array', async (t) => { + + const result = aggregateRecentFields(uuid(), 'siteReferrer') + + t.true(Array.isArray(result)) + +}) \ No newline at end of file diff --git a/test/aggregations/aggregateTopFields.js b/test/aggregations/aggregateTopFields.js new file mode 100644 index 00000000..6e278877 --- /dev/null +++ b/test/aggregations/aggregateTopFields.js @@ -0,0 +1,14 @@ +'use strict' + +const test = require('ava') +const uuid = require('uuid/v4') + +const aggregateTopFields = require('../../src/aggregations/aggregateTopFields') + +test('return array', async (t) => { + + const result = aggregateTopFields(uuid(), 'siteReferrer') + + t.true(Array.isArray(result)) + +}) \ No newline at end of file diff --git a/test/aggregations/aggregateYearlyViews.js b/test/aggregations/aggregateYearlyViews.js new file mode 100644 index 00000000..cdb8a2ba --- /dev/null +++ b/test/aggregations/aggregateYearlyViews.js @@ -0,0 +1,22 @@ +'use strict' + +const test = require('ava') +const uuid = require('uuid/v4') + +const aggregateYearlyViews = require('../../src/aggregations/aggregateYearlyViews') + +test('return unique aggregation', async (t) => { + + const result = aggregateYearlyViews(uuid(), true) + + t.true(Array.isArray(result)) + +}) + +test('return non-unique aggregation', async (t) => { + + const result = aggregateYearlyViews(uuid(), false) + + t.true(Array.isArray(result)) + +}) \ No newline at end of file diff --git a/test/constants/durations.js b/test/constants/durations.js new file mode 100644 index 00000000..d32f7d29 --- /dev/null +++ b/test/constants/durations.js @@ -0,0 +1,11 @@ +'use strict' + +const test = require('ava') + +const durations = require('../../src/constants/durations') + +test('is an object', async (t) => { + + t.is(typeof durations, 'object') + +}) \ No newline at end of file diff --git a/test/constants/languages.js b/test/constants/languages.js new file mode 100644 index 00000000..4d07af49 --- /dev/null +++ b/test/constants/languages.js @@ -0,0 +1,11 @@ +'use strict' + +const test = require('ava') + +const languages = require('../../src/constants/languages') + +test('is an object', async (t) => { + + t.is(typeof languages, 'object') + +}) \ No newline at end of file diff --git a/test/constants/pages.js b/test/constants/pages.js new file mode 100644 index 00000000..da06e1e1 --- /dev/null +++ b/test/constants/pages.js @@ -0,0 +1,11 @@ +'use strict' + +const test = require('ava') + +const pages = require('../../src/constants/pages') + +test('is an object', async (t) => { + + t.is(typeof pages, 'object') + +}) \ No newline at end of file diff --git a/test/constants/sizes.js b/test/constants/sizes.js new file mode 100644 index 00000000..d36b3961 --- /dev/null +++ b/test/constants/sizes.js @@ -0,0 +1,11 @@ +'use strict' + +const test = require('ava') + +const sizes = require('../../src/constants/sizes') + +test('is an object', async (t) => { + + t.is(typeof sizes, 'object') + +}) \ No newline at end of file diff --git a/test/constants/views.js b/test/constants/views.js new file mode 100644 index 00000000..46ff79da --- /dev/null +++ b/test/constants/views.js @@ -0,0 +1,11 @@ +'use strict' + +const test = require('ava') + +const views = require('../../src/constants/views') + +test('is an object', async (t) => { + + t.is(typeof views, 'object') + +}) \ No newline at end of file diff --git a/test/server-with-cors.js b/test/server-with-cors.js index d4bb5070..b38c3862 100644 --- a/test/server-with-cors.js +++ b/test/server-with-cors.js @@ -9,20 +9,21 @@ const server = require('../src/server') const base = listen(server) -test('return no cors headers if env var specifies none', async (t) => { +test('return cors headers if env var specifies one', async (t) => { + + const url = new URL(await base) const restore = mockedEnv({ - ACKEE_ALLOW_ORIGIN: 'https://example.com' + ACKEE_ALLOW_ORIGIN: url.origin }) - const url = new URL(await base) const res = await fetch(url.href) const headers = res.headers - t.is(headers.get('Access-Control-Allow-Origin'), 'https://example.com') + t.is(headers.get('Access-Control-Allow-Origin'), url.origin) t.is(headers.get('Access-Control-Allow-Methods'), 'GET, POST, PATCH, OPTIONS') t.is(headers.get('Access-Control-Allow-Headers'), 'Content-Type') restore() -}) \ No newline at end of file +}) diff --git a/test/server-with-multiple-cors.js b/test/server-with-multiple-cors.js new file mode 100644 index 00000000..77fe1813 --- /dev/null +++ b/test/server-with-multiple-cors.js @@ -0,0 +1,29 @@ +'use strict' + +const test = require('ava') +const listen = require('test-listen') +const fetch = require('node-fetch') +const mockedEnv = require('mocked-env') + +const server = require('../src/server') + +const base = listen(server) + +test('return cors headers with corresponding origin if env var specifies multiple origins', async (t) => { + + const url = new URL(await base) + + const restore = mockedEnv({ + ACKEE_ALLOW_ORIGIN: `https://example.com,${ url.origin }` + }) + + const res = await fetch(url.href) + const headers = res.headers + + t.is(headers.get('Access-Control-Allow-Origin'), url.origin) + t.is(headers.get('Access-Control-Allow-Methods'), 'GET, POST, PATCH, OPTIONS') + t.is(headers.get('Access-Control-Allow-Headers'), 'Content-Type') + + restore() + +}) diff --git a/test/server-with-unlisted-cors.js b/test/server-with-unlisted-cors.js new file mode 100644 index 00000000..38d89c29 --- /dev/null +++ b/test/server-with-unlisted-cors.js @@ -0,0 +1,29 @@ +'use strict' + +const test = require('ava') +const listen = require('test-listen') +const fetch = require('node-fetch') +const mockedEnv = require('mocked-env') + +const server = require('../src/server') + +const base = listen(server) + +test('return cors headers with no origin if hostname not whitelisted in env var', async (t) => { + + const url = new URL(await base) + + const restore = mockedEnv({ + ACKEE_ALLOW_ORIGIN: `https://example.com` + }) + + const res = await fetch(url.href) + const headers = res.headers + + t.is(headers.get('Access-Control-Allow-Origin'), null) + t.is(headers.get('Access-Control-Allow-Methods'), null) + t.is(headers.get('Access-Control-Allow-Headers'), null) + + restore() + +}) diff --git a/test/server-with-wildcard-cors.js b/test/server-with-wildcard-cors.js new file mode 100644 index 00000000..d1f579b8 --- /dev/null +++ b/test/server-with-wildcard-cors.js @@ -0,0 +1,29 @@ +'use strict' + +const test = require('ava') +const listen = require('test-listen') +const fetch = require('node-fetch') +const mockedEnv = require('mocked-env') + +const server = require('../src/server') + +const base = listen(server) + +test('return cors headers if env vars specify wildcard', async (t) => { + + const url = new URL(await base) + + const restore = mockedEnv({ + ACKEE_ALLOW_ORIGIN: '*' + }) + + const res = await fetch(url.href) + const headers = res.headers + + t.is(headers.get('Access-Control-Allow-Origin'), '*') + t.is(headers.get('Access-Control-Allow-Methods'), 'GET, POST, PATCH, OPTIONS') + t.is(headers.get('Access-Control-Allow-Headers'), 'Content-Type') + + restore() + +}) diff --git a/test/utils/dateWithOffset.js b/test/utils/dateWithOffset.js deleted file mode 100644 index 4446c790..00000000 --- a/test/utils/dateWithOffset.js +++ /dev/null @@ -1,50 +0,0 @@ -'use strict' - -const test = require('ava') - -const { day } = require('../../src/utils/times') -const dateWithOffset = require('../../src/utils/dateWithOffset') - -test('return today', async (t) => { - - const todayDate = new Date() - - const resultDate = dateWithOffset(0) - - t.is(resultDate.getDate(), todayDate.getDate()) - t.is(resultDate.getHours(), 0) - t.is(resultDate.getMinutes(), 0) - t.is(resultDate.getSeconds(), 0) - t.is(resultDate.getMilliseconds(), 0) - -}) - -test('return tomorrow', async (t) => { - - const tomorrowTimestamp = Date.now() + day - const tomorrowDate = new Date(tomorrowTimestamp) - - const resultDate = dateWithOffset(1) - - t.is(resultDate.getDate(), tomorrowDate.getDate()) - t.is(resultDate.getHours(), 0) - t.is(resultDate.getMinutes(), 0) - t.is(resultDate.getSeconds(), 0) - t.is(resultDate.getMilliseconds(), 0) - -}) - -test('return yesterday', async (t) => { - - const yesterdayTimestamp = Date.now() - day - const yesterdayDate = new Date(yesterdayTimestamp) - - const resultDate = dateWithOffset(-1) - - t.is(resultDate.getDate(), yesterdayDate.getDate()) - t.is(resultDate.getHours(), 0) - t.is(resultDate.getMinutes(), 0) - t.is(resultDate.getSeconds(), 0) - t.is(resultDate.getMilliseconds(), 0) - -}) \ No newline at end of file diff --git a/test/utils/zeroDate.js b/test/utils/zeroDate.js new file mode 100644 index 00000000..1b5fd515 --- /dev/null +++ b/test/utils/zeroDate.js @@ -0,0 +1,16 @@ +'use strict' + +const test = require('ava') + +const zeroDate = require('../../src/utils/zeroDate') + +test('return date without hours, minutes, seconds and milliseconds', async (t) => { + + const date = zeroDate() + + t.is(date.getHours(), 0) + t.is(date.getMinutes(), 0) + t.is(date.getSeconds(), 0) + t.is(date.getMilliseconds(), 0) + +}) \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 78bda0e4..dd4afb2d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -877,10 +877,10 @@ resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw== -"@xobotyi/scrollbar-width@1.8.2": - version "1.8.2" - resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.8.2.tgz#056946ac41ade4885c576619c8d70c46c77e9683" - integrity sha512-RV6+4hR29oMaPCvSYFUvzOvlsrg2s2k5NE9tNERs+4nFIC9dRXxs+lL2CcaRTbl3yQxKwAZ8Cd+qMI8aUu9TFw== +"@xobotyi/scrollbar-width@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.0.tgz#2a5d02f15c7f5624339e5d690aba432bfd9e79f0" + integrity sha512-W8oNXd3HkW9eQHxk+47iRx4aqd0yIV9NoeykUTd0uE0sYx3LOAQE7rfHOd8xtMP7IADfLIdG0o0H1sXvHUF7dw== JSONStream@^1.0.3: version "1.3.5" @@ -2240,6 +2240,11 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +date-fns@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.9.0.tgz#d0b175a5c37ed5f17b97e2272bbc1fa5aec677d2" + integrity sha512-khbFLu/MlzLjEzy9Gh8oY1hNt/Dvxw3J6Rbc28cVoYWQaC1S3YI4xwkF9ZWcjDLscbZlY9hISMr66RFzZagLsA== + date-time@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/date-time/-/date-time-2.1.0.tgz#0286d1b4c769633b3ca13e1e62558d2dbdc2eba2" @@ -2639,10 +2644,10 @@ eslint-plugin-import@^2.20.0: read-pkg-up "^2.0.0" resolve "^1.12.0" -eslint-plugin-react-hooks@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.3.0.tgz#53e073961f1f5ccf8dd19558036c1fac8c29d99a" - integrity sha512-gLKCa52G4ee7uXzdLiorca7JIQZPPXRAQDXV83J4bUEeUuc5pIEyZYAZ45Xnxe5IuupxEqHS+hUhSLIimK1EMw== +eslint-plugin-react-hooks@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.4.0.tgz#db6ee1cc953e3a217035da3d4e9d4356d3c672a4" + integrity sha512-bH5DOCP6WpuOqNaux2BlaDCrSgv8s5BitP90bTgtZ1ZsRn2bdIfeMDY5F2RnJVnyKDy6KRQRDbipPLZ1y77QtQ== eslint-plugin-react-native-globals@^0.1.1: version "0.1.2" @@ -4471,10 +4476,10 @@ mongoose-legacy-pluralize@1.0.2: resolved "https://registry.yarnpkg.com/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz#3ba9f91fa507b5186d399fb40854bff18fb563e4" integrity sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ== -mongoose@^5.8.9: - version "5.9.0" - resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-5.9.0.tgz#0a707c3716faea01416708e9b7676f92160d5950" - integrity sha512-vAoQC6RAX5NqXX+H0WKRsvI0gUj3OhFwsu7JHKwsLQ3cNvE7ZfpeG5aDBvbx9XaW0a+Z2ZqysQpktJhSIzLKtg== +mongoose@^5.9.1: + version "5.9.1" + resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-5.9.1.tgz#48a5fafe6bc7b57d6b41b12ebf55fa5f609518d6" + integrity sha512-qgS31/nZ63vpr8yBg6w8vaV8ITxwrF2ioNW5AakXmqvVBaOsI0xpDd5QBowESy2InDTk+iDaN5SNgSxGG6GntQ== dependencies: bson "~1.1.1" kareem "2.3.1" @@ -5639,30 +5644,34 @@ react-dom@^16.12.0: prop-types "^15.6.2" scheduler "^0.18.0" +react-error-boundary@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-1.2.5.tgz#a362cb799d2e58ff8f114f7c4bc25677ce4e4149" + integrity sha512-5CPSeLJA2igJNppAgFRwnTL9aK3ojenk65enNzhVyoxYNbHpIJXnChUO7+4vPhkncRA9wvQMXq6Azp2XeXd+iQ== + react-is@^16.7.0, react-is@^16.8.1, react-is@^16.9.0: version "16.12.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c" integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q== -react-redux@^7.1.3: - version "7.1.3" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.1.3.tgz#717a3d7bbe3a1b2d535c94885ce04cdc5a33fc79" - integrity sha512-uI1wca+ECG9RoVkWQFF4jDMqmaw0/qnvaSvOoL/GA4dNxf6LoV8sUAcNDvE5NWKs4hFpn0t6wswNQnY3f7HT3w== +react-redux@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.0.tgz#f970f62192b3981642fec46fd0db18a074fe879d" + integrity sha512-EvCAZYGfOLqwV7gh849xy9/pt55rJXPwmYvI4lilPM5rUT/1NxuuN59ipdBksRVSvz0KInbPnp4IfoXJXCqiDA== dependencies: "@babel/runtime" "^7.5.5" hoist-non-react-statics "^3.3.0" - invariant "^2.2.4" loose-envify "^1.4.0" prop-types "^15.7.2" react-is "^16.9.0" -react-use@^13.26.0: - version "13.26.0" - resolved "https://registry.yarnpkg.com/react-use/-/react-use-13.26.0.tgz#61559001c7ac193d3427578878dcbdc84ad0457e" - integrity sha512-PM11qqLAMjJRihmky+TqJc1SIZyHYTZNZo3D5LN65amxYpkbSg5DFNWh9QOrsFN1kzG+jWh5WDyHNNkzgdHhTw== +react-use@^13.26.1: + version "13.26.1" + resolved "https://registry.yarnpkg.com/react-use/-/react-use-13.26.1.tgz#a26e51b26ebe1a3a00cadfe4d7f15c25bb19780b" + integrity sha512-hDc4s8w4WI8G7c1BX+IsrdQFcZPfCHE/6oLpGPtcIPoxVhwj4QvVmNE8RnsnddBJ57HN8Xvkc3jp/8Z/4OB53w== dependencies: "@types/js-cookie" "2.2.4" - "@xobotyi/scrollbar-width" "1.8.2" + "@xobotyi/scrollbar-width" "1.9.0" copy-to-clipboard "^3.2.0" fast-deep-equal "^3.1.1" fast-shallow-equal "^1.0.0"