-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(server): consolidate feedback stats (#713)
- Loading branch information
Showing
3 changed files
with
109 additions
and
40 deletions.
There are no files selected for viewing
48 changes: 8 additions & 40 deletions
48
server/src/feedback/feedback-stats/feedback-stats.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,60 +1,28 @@ | ||
import { Injectable } from '@nestjs/common'; | ||
import { FeedbackDbService } from '../feedback-db/feedback-db.service'; | ||
|
||
type FeedbackHistory = { | ||
createdAt: number; | ||
updatedAt: number; | ||
requested: boolean; | ||
shared: boolean; | ||
status: 'pending' | 'done'; | ||
}; | ||
import { FeedbackHistory } from './feedback-stats.types'; | ||
import { buildHistoryByMonth, buildHistoryByMonthStats, buildHistoryStats } from './feedback-stats.utils'; | ||
|
||
@Injectable() | ||
export class FeedbackStatsService { | ||
/** | ||
* List of unique users who have given at least 1 feedback | ||
*/ | ||
private giverEmailList = new Set<string>(); | ||
|
||
/** | ||
* List of unique users who have received at least 1 feedback | ||
*/ | ||
private receiverEmailList = new Set<string>(); | ||
|
||
/** | ||
* List of unique users who have given of received at least 1 feedback | ||
*/ | ||
private allEmailList = new Set<string>(); | ||
|
||
private history = new Map<string, FeedbackHistory>(); | ||
|
||
constructor(private feedbackDbService: FeedbackDbService) { | ||
this.feedbackDbService.onFeedbackChanges((feedbacks) => { | ||
feedbacks.forEach(({ giverEmail, receiverEmail, requested, status }) => { | ||
// For feedback requests, the `giverEmail` is not to be taken into account until he has replied. | ||
if (!requested || status === 'done') { | ||
this.giverEmailList.add(giverEmail); | ||
this.allEmailList.add(giverEmail); | ||
} | ||
|
||
this.receiverEmailList.add(receiverEmail); | ||
this.allEmailList.add(receiverEmail); | ||
}); | ||
|
||
feedbacks.forEach(({ id, createdAt, updatedAt, requested, shared, status }) => | ||
this.history.set(id, { createdAt, updatedAt, requested, shared, status }), | ||
feedbacks.forEach(({ id, giverEmail, receiverEmail, shared, requested, status, createdAt, updatedAt }) => | ||
this.history.set(id, { giverEmail, receiverEmail, shared, requested, status, createdAt, updatedAt }), | ||
); | ||
}); | ||
} | ||
|
||
// Note that the first call to this method will trigger service creation and data supply. | ||
// As the method's return is synchronous, it's possible that there will be no data for the first few calls. | ||
getStats() { | ||
const history = Array.from(this.history.values()); | ||
|
||
return { | ||
numberOfUniqueGivers: this.giverEmailList.size, | ||
numberOfUniqueReceivers: this.receiverEmailList.size, | ||
numberOfUniqueUsers: this.allEmailList.size, | ||
history: Array.from(this.history.values()), | ||
summary: buildHistoryStats(history), | ||
details: buildHistoryByMonthStats(buildHistoryByMonth(history)), | ||
}; | ||
} | ||
} |
24 changes: 24 additions & 0 deletions
24
server/src/feedback/feedback-stats/feedback-stats.types.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
export type FeedbackHistory = { | ||
giverEmail: string; | ||
receiverEmail: string; | ||
shared: boolean; | ||
requested: boolean; | ||
status: 'pending' | 'done'; | ||
createdAt: number; | ||
updatedAt: number; | ||
}; | ||
|
||
export type FeedbackHistoryStats = { | ||
// User stats | ||
uniqueGivers: number; | ||
uniqueReceivers: number; | ||
uniqueUsers: number; | ||
|
||
// Feedback stats | ||
spontaneousFeedback: number; | ||
requestedFeedbackDone: number; | ||
requestedFeedbackPending: number; | ||
sharedFeedback: number; | ||
}; | ||
|
||
export type FeedbackMonthHistoryStats = { month: string } & FeedbackHistoryStats; |
77 changes: 77 additions & 0 deletions
77
server/src/feedback/feedback-stats/feedback-stats.utils.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import { FeedbackHistory, FeedbackHistoryStats, FeedbackMonthHistoryStats } from './feedback-stats.types'; | ||
|
||
export const buildHistoryStats = (history: FeedbackHistory[]): FeedbackHistoryStats => { | ||
// List of unique users who have given at least 1 feedback | ||
const giverEmailList = new Set<string>(); | ||
|
||
// List of unique users who have received at least 1 feedback (including users with pending feedback requests) | ||
const receiverEmailList = new Set<string>(); | ||
|
||
// List of unique users who have given, received or requested at least 1 feedback | ||
const allEmailList = new Set<string>(); | ||
|
||
let spontaneousFeedback = 0; | ||
let requestedFeedbackDone = 0; | ||
let requestedFeedbackPending = 0; | ||
let sharedFeedback = 0; | ||
|
||
history.forEach(({ giverEmail, receiverEmail, requested, status, shared }) => { | ||
// For feedback requests, the `giverEmail` is not to be taken into account until he has replied. | ||
if (!requested || status === 'done') { | ||
giverEmailList.add(giverEmail); | ||
allEmailList.add(giverEmail); | ||
} | ||
|
||
receiverEmailList.add(receiverEmail); | ||
allEmailList.add(receiverEmail); | ||
|
||
if (!requested) { | ||
spontaneousFeedback += 1; | ||
} else if (status === 'done') { | ||
requestedFeedbackDone += 1; | ||
} else { | ||
requestedFeedbackPending += 1; | ||
} | ||
|
||
if (shared) { | ||
sharedFeedback += 1; | ||
} | ||
}); | ||
|
||
return { | ||
uniqueGivers: giverEmailList.size, | ||
uniqueReceivers: receiverEmailList.size, | ||
uniqueUsers: allEmailList.size, | ||
|
||
spontaneousFeedback, | ||
requestedFeedbackDone, | ||
requestedFeedbackPending, | ||
sharedFeedback, | ||
}; | ||
}; | ||
|
||
// timestamp -> 1728166612791 | ||
// toLocaleDateString('fr-FR') -> '06/10/2024' | ||
// split/reverse/slice/join -> '2024-10' | ||
const toYYYMM = (timestamp: number) => | ||
new Date(timestamp).toLocaleDateString('fr-FR').split('/').reverse().slice(0, 2).join('-'); | ||
|
||
export const buildHistoryByMonth = (history: FeedbackHistory[]): Record<string, FeedbackHistory[]> => | ||
history.reduce( | ||
(historyByMonth, event) => { | ||
const month = toYYYMM(!event.requested || event.status === 'done' ? event.updatedAt : event.createdAt); | ||
|
||
historyByMonth[month] ??= []; | ||
historyByMonth[month].push(event); | ||
|
||
return historyByMonth; | ||
}, | ||
{} as Record<string, FeedbackHistory[]>, | ||
); | ||
|
||
export const buildHistoryByMonthStats = ( | ||
historyByMonth: Record<string, FeedbackHistory[]>, | ||
): FeedbackMonthHistoryStats[] => | ||
Object.entries(historyByMonth) | ||
.map(([month, history]) => ({ month, ...buildHistoryStats(history) })) | ||
.sort(({ month: a }, { month: b }) => (a < b ? -1 : a > b ? 1 : 0)); |