From f6763e12dce0be8032c61b32793d87c78c491a1f Mon Sep 17 00:00:00 2001 From: Georgy Berezhnoy Date: Sun, 5 Dec 2021 04:00:17 +0300 Subject: [PATCH 1/4] Add affected users chart --- src/models/eventsFactory.js | 90 +++++++++++++++++++++++++++++++------ src/resolvers/event.js | 15 +++++++ src/typeDefs/event.ts | 21 +++++++++ 3 files changed, 112 insertions(+), 14 deletions(-) diff --git a/src/models/eventsFactory.js b/src/models/eventsFactory.js index 1a380b9f..db7ce221 100644 --- a/src/models/eventsFactory.js +++ b/src/models/eventsFactory.js @@ -258,16 +258,21 @@ class EventsFactory extends Factory { } /** - * Fetch timestamps and total count of errors (or target error) for each day since + * Get daily events for last few days grouped by user timezone days * - * @param {number} days - how many days we need to fetch for displaying in a chart - * @param {number} timezoneOffset - user's local timezone offset in minutes - * @param {string} groupHash - event's group hash for showing only target event - * @return {ProjectChartItem[]} + * @param {number} days - days to get + * @param {number} [timezoneOffset] - user timezone offset in minutes + * @param {string} [groupHash] - event group hash + * @return {Promise} */ - async findChartData(days, timezoneOffset = 0, groupHash = '') { + async getGroupedDailyEvents(days, timezoneOffset = 0, groupHash) { const today = new Date(); - const since = today.setDate(today.getDate() - days) / 1000; + + today.setHours(0, 0, 0, 0); + + const since = new Date(); + + since.setTime(today.getTime() + timezoneOffset * 60 * 1000 - days * 24 * 60 * 60 * 1000); /** * Compose options for find method @@ -275,7 +280,7 @@ class EventsFactory extends Factory { */ const options = { groupingTimestamp: { - $gt: since, + $gt: since / 1000, }, }; @@ -296,8 +301,12 @@ class EventsFactory extends Factory { * Convert UTC midnight to midnights in user's timezone */ dailyEvents = dailyEvents.map((item) => { + const groupingTimestamp = new Date(item.groupingTimestamp * 1000); + + groupingTimestamp.setTime(groupingTimestamp.getTime() + timezoneOffset * 60 * 1000); + return Object.assign({}, item, { - groupingTimestamp: getMidnightWithTimezoneOffset(item.lastRepetitionTime, item.groupingTimestamp, timezoneOffset), + groupingTimestamp: groupingTimestamp / 1000, }); }); @@ -305,7 +314,19 @@ class EventsFactory extends Factory { * Group events using 'groupByTimestamp:NNNNNNNN' key * @type {ProjectChartItem[]} */ - const groupedData = groupBy('groupingTimestamp')(dailyEvents); + return groupBy('groupingTimestamp')(dailyEvents); + } + + /** + * Fetch timestamps and count of affected users for each day since + * + * @param {number} days - how many days we need to fetch for displaying in a chart + * @param {number} timezoneOffset - user's local timezone offset in minutes + * @param {string} groupHash - event's group hash for showing only target event + * @return {AffectedUsersChartItem[]} + */ + async findAffectedUsersChart(days, timezoneOffset = 0, groupHash = '') { + const groupedData = await this.getGroupedDailyEvents(days, timezoneOffset, groupHash); /** * Now fill all requested days @@ -314,12 +335,53 @@ class EventsFactory extends Factory { for (let i = 0; i < days; i++) { const now = new Date(); - const day = new Date(now.setDate(now.getDate() - i)); - const dayMidnight = getUTCMidnight(day) / 1000; - const groupedEvents = groupedData[`groupingTimestamp:${dayMidnight}`]; + + now.setHours(0, 0, 0, 0); + now.setTime(now.getTime() + timezoneOffset * 60 * 1000 - i * 24 * 60 * 60 * 1000); + + const groupedEvents = groupedData[`groupingTimestamp:${now / 1000}`]; + const affectedUsers = groupedEvents ? groupedEvents.reduce((set, value) => new Set([...set, ...value.affectedUsers]), new Set()) : new Set(); + + result.push({ + timestamp: now / 1000, + count: affectedUsers.size, + }); + } + + /** + * Order by time ascendance + */ + result = result.sort((a, b) => a.timestamp - b.timestamp); + + return result; + } + + /** + * Fetch timestamps and total count of errors (or target error) for each day since + * + * @param {number} days - how many days we need to fetch for displaying in a chart + * @param {number} timezoneOffset - user's local timezone offset in minutes + * @param {string} groupHash - event's group hash for showing only target event + * @return {ProjectChartItem[]} + */ + async findChartData(days, timezoneOffset = 0, groupHash = '') { + const groupedData = await this.getGroupedDailyEvents(days, timezoneOffset, groupHash); + + /** + * Now fill all requested days + */ + let result = []; + + for (let i = 0; i < days; i++) { + const now = new Date(); + + now.setHours(0, 0, 0, 0); + now.setTime(now.getTime() + timezoneOffset * 60 * 1000 - i * 24 * 60 * 60 * 1000); + + const groupedEvents = groupedData[`groupingTimestamp:${now / 1000}`]; result.push({ - timestamp: dayMidnight, + timestamp: now / 1000, count: groupedEvents ? groupedEvents.reduce((sum, value) => sum + value.count, 0) : 0, }); } diff --git a/src/resolvers/event.js b/src/resolvers/event.js index 6faaeb16..1fe00690 100644 --- a/src/resolvers/event.js +++ b/src/resolvers/event.js @@ -89,6 +89,21 @@ module.exports = { return factories.usersFactory.dataLoaders.userById.load(assignee); }, + /** + * Return chart data for target event affected users in last few days + * + * @param {string} projectId - event's project + * @param {string} groupHash - event's groupHash + * @param {number} days - how many days we need to fetch for displaying in a charts + * @param {number} timezoneOffset - user's local timezone offset in minutes + * @returns {Promise} + */ + async usersAffectedChart({ projectId, groupHash }, { days, timezoneOffset }) { + const factory = new EventsFactory(new ObjectID(projectId)); + + return factory.findAffectedUsersChart(days, timezoneOffset, groupHash); + }, + /** * Return chart data for target event occured in last few days * diff --git a/src/typeDefs/event.ts b/src/typeDefs/event.ts index 8baad046..4dfedd7a 100644 --- a/src/typeDefs/event.ts +++ b/src/typeDefs/event.ts @@ -348,6 +348,21 @@ type Event { """ usersAffected: Int + """ + Return affected users chart data for the last few days + """ + usersAffectedChart( + """ + How many days we need to fetch for displaying in a chart + """ + days: Int! = 0 + + """ + User's local timezone offset in minutes + """ + timezoneOffset: Int! = 0 + ): [ChartDataItem!]! @requireAuth + """ Return graph of the error rate for the last few days """ @@ -392,6 +407,12 @@ type DailyEventInfo { Last event occurrence timestamp """ lastRepetitionTime: Float! + + + """ + Array of user's ids affected this day + """ + affectedUsers: [String!] } type Subscription { From 0e8af6115d46f7839843934802889fb7911a22fd Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 5 Dec 2021 01:01:16 +0000 Subject: [PATCH 2/4] Bump version up to 1.0.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 69375d18..e55a38aa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hawk.api", - "version": "1.0.6", + "version": "1.0.7", "main": "index.ts", "license": "UNLICENSED", "scripts": { From 0126cc46e5968a767b82eec6bcbd7706ee107365 Mon Sep 17 00:00:00 2001 From: Georgy Berezhnoy Date: Sun, 5 Dec 2021 04:05:09 +0300 Subject: [PATCH 3/4] Fix lint --- src/models/eventsFactory.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/models/eventsFactory.js b/src/models/eventsFactory.js index db7ce221..80f4de87 100644 --- a/src/models/eventsFactory.js +++ b/src/models/eventsFactory.js @@ -1,4 +1,3 @@ -import { getMidnightWithTimezoneOffset, getUTCMidnight } from '../utils/dates'; import { groupBy } from '../utils/grouper'; const Factory = require('./modelFactory'); From 199c8fa9df924aa8eb00cc623cbda7037624c17b Mon Sep 17 00:00:00 2001 From: Georgy Berezhnoy Date: Sun, 26 Dec 2021 02:24:48 +0300 Subject: [PATCH 4/4] Fixes for charts dates --- src/models/eventsFactory.js | 67 ++++++++++++++++++++++++------------- src/utils/dates.ts | 23 ++++++++++++- 2 files changed, 66 insertions(+), 24 deletions(-) diff --git a/src/models/eventsFactory.js b/src/models/eventsFactory.js index 80f4de87..9c9a6d0c 100644 --- a/src/models/eventsFactory.js +++ b/src/models/eventsFactory.js @@ -1,3 +1,4 @@ +import { addTimezoneOffset, getMidnightWithTimezoneOffset, setUTCMidnight } from '../utils/dates'; import { groupBy } from '../utils/grouper'; const Factory = require('./modelFactory'); @@ -266,12 +267,7 @@ class EventsFactory extends Factory { */ async getGroupedDailyEvents(days, timezoneOffset = 0, groupHash) { const today = new Date(); - - today.setHours(0, 0, 0, 0); - - const since = new Date(); - - since.setTime(today.getTime() + timezoneOffset * 60 * 1000 - days * 24 * 60 * 60 * 1000); + const since = today.setDate(today.getDate() - days) / 1000; /** * Compose options for find method @@ -279,7 +275,7 @@ class EventsFactory extends Factory { */ const options = { groupingTimestamp: { - $gt: since / 1000, + $gt: since, }, }; @@ -300,12 +296,8 @@ class EventsFactory extends Factory { * Convert UTC midnight to midnights in user's timezone */ dailyEvents = dailyEvents.map((item) => { - const groupingTimestamp = new Date(item.groupingTimestamp * 1000); - - groupingTimestamp.setTime(groupingTimestamp.getTime() + timezoneOffset * 60 * 1000); - return Object.assign({}, item, { - groupingTimestamp: groupingTimestamp / 1000, + groupingTimestamp: getMidnightWithTimezoneOffset(item.lastRepetitionTime, item.groupingTimestamp, timezoneOffset), }); }); @@ -333,16 +325,31 @@ class EventsFactory extends Factory { let result = []; for (let i = 0; i < days; i++) { - const now = new Date(); + const day = new Date(); + + /** + * Subtract timezone offset to get user`s local day + * + * @example 22:00 UTC 25.12 === 01:00 GMT+03 26.12, so local date is 26 + */ + addTimezoneOffset(day, -timezoneOffset); + + /** + * Set midnight for user`s local date + */ + setUTCMidnight(day); - now.setHours(0, 0, 0, 0); - now.setTime(now.getTime() + timezoneOffset * 60 * 1000 - i * 24 * 60 * 60 * 1000); + /** + * Get date for the chart + */ + day.setDate(day.getDate() - i); - const groupedEvents = groupedData[`groupingTimestamp:${now / 1000}`]; + const dayMidnight = day / 1000; + const groupedEvents = groupedData[`groupingTimestamp:${dayMidnight}`]; const affectedUsers = groupedEvents ? groupedEvents.reduce((set, value) => new Set([...set, ...value.affectedUsers]), new Set()) : new Set(); result.push({ - timestamp: now / 1000, + timestamp: dayMidnight, count: affectedUsers.size, }); } @@ -365,22 +372,36 @@ class EventsFactory extends Factory { */ async findChartData(days, timezoneOffset = 0, groupHash = '') { const groupedData = await this.getGroupedDailyEvents(days, timezoneOffset, groupHash); - /** * Now fill all requested days */ let result = []; for (let i = 0; i < days; i++) { - const now = new Date(); + const day = new Date(); + + /** + * Subtract timezone offset to get user`s local day + * + * @example 22:00 UTC 25.12 === 01:00 GMT+03 26.12, so local date is 26 + */ + addTimezoneOffset(day, -timezoneOffset); - now.setHours(0, 0, 0, 0); - now.setTime(now.getTime() + timezoneOffset * 60 * 1000 - i * 24 * 60 * 60 * 1000); + /** + * Set midnight for user`s local date + */ + setUTCMidnight(day); + + /** + * Get date for the chart + */ + day.setDate(day.getDate() - i); - const groupedEvents = groupedData[`groupingTimestamp:${now / 1000}`]; + const dayMidnight = day / 1000; + const groupedEvents = groupedData[`groupingTimestamp:${dayMidnight}`]; result.push({ - timestamp: now / 1000, + timestamp: dayMidnight, count: groupedEvents ? groupedEvents.reduce((sum, value) => sum + value.count, 0) : 0, }); } diff --git a/src/utils/dates.ts b/src/utils/dates.ts index c2ee05cf..cde5ed39 100644 --- a/src/utils/dates.ts +++ b/src/utils/dates.ts @@ -6,7 +6,28 @@ import { ObjectId } from 'mongodb'; * @param {Date} date - date object */ export function getUTCMidnight(date: Date): number { - return date.setUTCHours(0, 0, 0, 0); + const copy = new Date(date); + + return copy.setUTCHours(0, 0, 0, 0); +} + +/** + * Sets UTC midnight for a given date + * + * @param {Date} date — date to set midnight + */ +export function setUTCMidnight(date: Date): void { + date.setHours(0, 0, 0, 0); +} + +/** + * Adds passed offset in minutes to a given date + * + * @param {Date} date — date to change + * @param {number} offset — offset in minutes + */ +export function addTimezoneOffset(date: Date, offset: number): void { + date.setTime(date.getTime() + offset * 60 * 1000); } /**