Skip to content

Commit

Permalink
feat(server): consolidate feedback stats (#713)
Browse files Browse the repository at this point in the history
  • Loading branch information
avine authored Oct 6, 2024
1 parent c69f7aa commit 2fce834
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 40 deletions.
48 changes: 8 additions & 40 deletions server/src/feedback/feedback-stats/feedback-stats.service.ts
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 server/src/feedback/feedback-stats/feedback-stats.types.ts
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 server/src/feedback/feedback-stats/feedback-stats.utils.ts
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));

0 comments on commit 2fce834

Please sign in to comment.