From 5e4956f3a20fbd8b91b52bbd875e12d91933f51a Mon Sep 17 00:00:00 2001 From: Ephraim Duncan <55143799+ephraimduncan@users.noreply.github.com> Date: Tue, 1 Apr 2025 11:12:41 +0000 Subject: [PATCH] fix: zero month addition (#1733) - Add zero month at the begining of each metric on the open page --- apps/openpage-api/lib/add-zero-month.ts | 54 +++++++++++ .../growth/get-monthly-completed-document.ts | 4 +- .../lib/growth/get-signer-conversion.ts | 4 +- .../lib/growth/get-user-monthly-growth.ts | 4 +- apps/openpage-api/lib/transform-data.ts | 90 ++++++++++++++----- 5 files changed, 132 insertions(+), 24 deletions(-) create mode 100644 apps/openpage-api/lib/add-zero-month.ts diff --git a/apps/openpage-api/lib/add-zero-month.ts b/apps/openpage-api/lib/add-zero-month.ts new file mode 100644 index 000000000..eb5914599 --- /dev/null +++ b/apps/openpage-api/lib/add-zero-month.ts @@ -0,0 +1,54 @@ +import { DateTime } from 'luxon'; + +export interface TransformedData { + labels: string[]; + datasets: Array<{ + label: string; + data: number[]; + }>; +} + +export function addZeroMonth(transformedData: TransformedData): TransformedData { + const result = { + labels: [...transformedData.labels], + datasets: transformedData.datasets.map((dataset) => ({ + label: dataset.label, + data: [...dataset.data], + })), + }; + + if (result.labels.length === 0) { + return result; + } + + if (result.datasets.every((dataset) => dataset.data[0] === 0)) { + return result; + } + + try { + let firstMonth = DateTime.fromFormat(result.labels[0], 'MMM yyyy'); + if (!firstMonth.isValid) { + const formats = ['MMM yyyy', 'MMMM yyyy', 'MM/yyyy', 'yyyy-MM']; + + for (const format of formats) { + firstMonth = DateTime.fromFormat(result.labels[0], format); + if (firstMonth.isValid) break; + } + + if (!firstMonth.isValid) { + console.warn(`Could not parse date: "${result.labels[0]}"`); + return transformedData; + } + } + + const zeroMonth = firstMonth.minus({ months: 1 }).toFormat('MMM yyyy'); + result.labels.unshift(zeroMonth); + result.datasets.forEach((dataset) => { + dataset.data.unshift(0); + }); + + return result; + } catch (error) { + return transformedData; + } +} diff --git a/apps/openpage-api/lib/growth/get-monthly-completed-document.ts b/apps/openpage-api/lib/growth/get-monthly-completed-document.ts index 885842101..f429b0a54 100644 --- a/apps/openpage-api/lib/growth/get-monthly-completed-document.ts +++ b/apps/openpage-api/lib/growth/get-monthly-completed-document.ts @@ -3,6 +3,8 @@ import { DateTime } from 'luxon'; import { kyselyPrisma, sql } from '@documenso/prisma'; +import { addZeroMonth } from '../add-zero-month'; + export const getCompletedDocumentsMonthly = async (type: 'count' | 'cumulative' = 'count') => { const qb = kyselyPrisma.$kysely .selectFrom('Document') @@ -35,7 +37,7 @@ export const getCompletedDocumentsMonthly = async (type: 'count' | 'cumulative' ], }; - return transformedData; + return addZeroMonth(transformedData); }; export type GetCompletedDocumentsMonthlyResult = Awaited< diff --git a/apps/openpage-api/lib/growth/get-signer-conversion.ts b/apps/openpage-api/lib/growth/get-signer-conversion.ts index aca2decb8..c70600179 100644 --- a/apps/openpage-api/lib/growth/get-signer-conversion.ts +++ b/apps/openpage-api/lib/growth/get-signer-conversion.ts @@ -2,6 +2,8 @@ import { DateTime } from 'luxon'; import { kyselyPrisma, sql } from '@documenso/prisma'; +import { addZeroMonth } from '../add-zero-month'; + export const getSignerConversionMonthly = async (type: 'count' | 'cumulative' = 'count') => { const qb = kyselyPrisma.$kysely .selectFrom('Recipient') @@ -34,7 +36,7 @@ export const getSignerConversionMonthly = async (type: 'count' | 'cumulative' = ], }; - return transformedData; + return addZeroMonth(transformedData); }; export type GetSignerConversionMonthlyResult = Awaited< diff --git a/apps/openpage-api/lib/growth/get-user-monthly-growth.ts b/apps/openpage-api/lib/growth/get-user-monthly-growth.ts index 6d4e526cb..9eba7311f 100644 --- a/apps/openpage-api/lib/growth/get-user-monthly-growth.ts +++ b/apps/openpage-api/lib/growth/get-user-monthly-growth.ts @@ -2,6 +2,8 @@ import { DateTime } from 'luxon'; import { kyselyPrisma, sql } from '@documenso/prisma'; +import { addZeroMonth } from '../add-zero-month'; + export const getUserMonthlyGrowth = async (type: 'count' | 'cumulative' = 'count') => { const qb = kyselyPrisma.$kysely .selectFrom('User') @@ -32,7 +34,7 @@ export const getUserMonthlyGrowth = async (type: 'count' | 'cumulative' = 'count ], }; - return transformedData; + return addZeroMonth(transformedData); }; export type GetUserMonthlyGrowthResult = Awaited>; diff --git a/apps/openpage-api/lib/transform-data.ts b/apps/openpage-api/lib/transform-data.ts index b5f0cd838..079ed4f6e 100644 --- a/apps/openpage-api/lib/transform-data.ts +++ b/apps/openpage-api/lib/transform-data.ts @@ -1,5 +1,7 @@ import { DateTime } from 'luxon'; +import { addZeroMonth } from './add-zero-month'; + type MetricKeys = { stars: number; forks: number; @@ -37,31 +39,77 @@ export function transformData({ data: DataEntry; metric: MetricKey; }): TransformData { - const sortedEntries = Object.entries(data).sort(([dateA], [dateB]) => { - const [yearA, monthA] = dateA.split('-').map(Number); - const [yearB, monthB] = dateB.split('-').map(Number); + try { + if (!data || Object.keys(data).length === 0) { + return { + labels: [], + datasets: [{ label: `Total ${FRIENDLY_METRIC_NAMES[metric]}`, data: [] }], + }; + } - return DateTime.local(yearA, monthA).toMillis() - DateTime.local(yearB, monthB).toMillis(); - }); + const sortedEntries = Object.entries(data).sort(([dateA], [dateB]) => { + try { + const [yearA, monthA] = dateA.split('-').map(Number); + const [yearB, monthB] = dateB.split('-').map(Number); - const labels = sortedEntries.map(([date]) => { - const [year, month] = date.split('-'); - const dateTime = DateTime.fromObject({ - year: Number(year), - month: Number(month), + if (isNaN(yearA) || isNaN(monthA) || isNaN(yearB) || isNaN(monthB)) { + console.warn(`Invalid date format: ${dateA} or ${dateB}`); + return 0; + } + + return DateTime.local(yearA, monthA).toMillis() - DateTime.local(yearB, monthB).toMillis(); + } catch (error) { + console.error('Error sorting entries:', error); + return 0; + } }); - return dateTime.toFormat('MMM yyyy'); - }); - return { - labels, - datasets: [ - { - label: `Total ${FRIENDLY_METRIC_NAMES[metric]}`, - data: sortedEntries.map(([_, stats]) => stats[metric]), - }, - ], - }; + const labels = sortedEntries.map(([date]) => { + try { + const [year, month] = date.split('-'); + + if (!year || !month || isNaN(Number(year)) || isNaN(Number(month))) { + console.warn(`Invalid date format: ${date}`); + return date; + } + + const dateTime = DateTime.fromObject({ + year: Number(year), + month: Number(month), + }); + + if (!dateTime.isValid) { + console.warn(`Invalid DateTime object for: ${date}`); + return date; + } + + return dateTime.toFormat('MMM yyyy'); + } catch (error) { + console.error('Error formatting date:', error, date); + return date; + } + }); + + const transformedData = { + labels, + datasets: [ + { + label: `Total ${FRIENDLY_METRIC_NAMES[metric]}`, + data: sortedEntries.map(([_, stats]) => { + const value = stats[metric]; + return typeof value === 'number' && !isNaN(value) ? value : 0; + }), + }, + ], + }; + + return addZeroMonth(transformedData); + } catch (error) { + return { + labels: [], + datasets: [{ label: `Total ${FRIENDLY_METRIC_NAMES[metric]}`, data: [] }], + }; + } } // To be on the safer side