diff --git a/frontend/src/container/BillingContainer/BillingContainer.tsx b/frontend/src/container/BillingContainer/BillingContainer.tsx
index 9b458013566..248819723c7 100644
--- a/frontend/src/container/BillingContainer/BillingContainer.tsx
+++ b/frontend/src/container/BillingContainer/BillingContainer.tsx
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-loop-func */
import './BillingContainer.styles.scss';
-import { CheckCircleOutlined } from '@ant-design/icons';
+import { CheckCircleOutlined, CloudDownloadOutlined } from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens';
import {
Alert,
@@ -40,6 +40,7 @@ import { isCloudUser } from 'utils/app';
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
import { BillingUsageGraph } from './BillingUsageGraph/BillingUsageGraph';
+import { prepareCsvData } from './BillingUsageGraph/utils';
interface DataType {
key: string;
@@ -371,6 +372,37 @@ export default function BillingContainer(): JSX.Element {
);
+ const handleCsvDownload = useCallback((): void => {
+ try {
+ const csv = prepareCsvData(apiResponse);
+
+ if (!csv.csvData || !csv.fileName) {
+ throw new Error('Invalid CSV data or file name.');
+ }
+
+ const csvBlob = new Blob([csv.csvData], { type: 'text/csv;charset=utf-8;' });
+ const csvUrl = URL.createObjectURL(csvBlob);
+ const downloadLink = document.createElement('a');
+
+ downloadLink.href = csvUrl;
+ downloadLink.download = csv.fileName;
+ document.body.appendChild(downloadLink); // Required for Firefox
+ downloadLink.click();
+
+ // Clean up
+ downloadLink.remove();
+ URL.revokeObjectURL(csvUrl); // Release the memory associated with the object URL
+ notifications.success({
+ message: 'Download successful',
+ });
+ } catch (error) {
+ console.error('Error downloading the CSV file:', error);
+ notifications.error({
+ message: SOMETHING_WENT_WRONG,
+ });
+ }
+ }, [apiResponse, notifications]);
+
return (
@@ -399,17 +431,29 @@ export default function BillingContainer(): JSX.Element {
) : null}
-
+
+ }
+ >
+ Download CSV
+
+
+
{licensesData?.payload?.onTrial &&
diff --git a/frontend/src/container/BillingContainer/BillingUsageGraph/generateCsvData.ts b/frontend/src/container/BillingContainer/BillingUsageGraph/generateCsvData.ts
new file mode 100644
index 00000000000..b70526aaa85
--- /dev/null
+++ b/frontend/src/container/BillingContainer/BillingUsageGraph/generateCsvData.ts
@@ -0,0 +1,129 @@
+import dayjs from 'dayjs';
+
+export interface QuantityData {
+ metric: string;
+ values: [number, number][];
+ queryName: string;
+ legend: string;
+ quantity: number[];
+ unit: string;
+}
+
+interface DataPoint {
+ date: string;
+ metric: {
+ total: number;
+ cost: number;
+ };
+ trace: {
+ total: number;
+ cost: number;
+ };
+ log: {
+ total: number;
+ cost: number;
+ };
+}
+
+interface CsvData {
+ Date: string;
+ 'Metrics Vol (Mn samples)': number;
+ 'Metrics Cost ($)': number;
+ 'Traces Vol (GBs)': number;
+ 'Traces Cost ($)': number;
+ 'Logs Vol (GBs)': number;
+ 'Logs Cost ($)': number;
+}
+
+const formatDate = (timestamp: number): string =>
+ dayjs.unix(timestamp).format('MM/DD/YYYY');
+
+const getQuantityData = (
+ data: QuantityData[],
+ metricName: string,
+): QuantityData => {
+ const defaultData: QuantityData = {
+ metric: metricName,
+ values: [],
+ queryName: metricName,
+ legend: metricName,
+ quantity: [],
+ unit: '',
+ };
+ return data.find((d) => d.metric === metricName) || defaultData;
+};
+
+const generateCsvData = (quantityData: QuantityData[]): any[] => {
+ const convertData = (data: QuantityData[]): DataPoint[] => {
+ const metricsData = getQuantityData(data, 'Metrics');
+ const tracesData = getQuantityData(data, 'Traces');
+ const logsData = getQuantityData(data, 'Logs');
+
+ const timestamps = metricsData.values.map((value) => value[0]);
+
+ return timestamps.map((timestamp, index) => {
+ const date = formatDate(timestamp);
+
+ return {
+ date,
+ metric: {
+ total: metricsData.quantity[index] ?? 0,
+ cost: metricsData.values[index]?.[1] ?? 0,
+ },
+ trace: {
+ total: tracesData.quantity[index] ?? 0,
+ cost: tracesData.values[index]?.[1] ?? 0,
+ },
+ log: {
+ total: logsData.quantity[index] ?? 0,
+ cost: logsData.values[index]?.[1] ?? 0,
+ },
+ };
+ });
+ };
+
+ const formattedData = convertData(quantityData);
+
+ // Calculate totals
+ const totals = formattedData.reduce(
+ (acc, dataPoint) => {
+ acc.metric.total += dataPoint.metric.total;
+ acc.metric.cost += dataPoint.metric.cost;
+ acc.trace.total += dataPoint.trace.total;
+ acc.trace.cost += dataPoint.trace.cost;
+ acc.log.total += dataPoint.log.total;
+ acc.log.cost += dataPoint.log.cost;
+ return acc;
+ },
+ {
+ metric: { total: 0, cost: 0 },
+ trace: { total: 0, cost: 0 },
+ log: { total: 0, cost: 0 },
+ },
+ );
+
+ const csvData: CsvData[] = formattedData.map((dataPoint) => ({
+ Date: dataPoint.date,
+ 'Metrics Vol (Mn samples)': parseFloat(dataPoint.metric.total.toFixed(2)),
+ 'Metrics Cost ($)': parseFloat(dataPoint.metric.cost.toFixed(2)),
+ 'Traces Vol (GBs)': parseFloat(dataPoint.trace.total.toFixed(2)),
+ 'Traces Cost ($)': parseFloat(dataPoint.trace.cost.toFixed(2)),
+ 'Logs Vol (GBs)': parseFloat(dataPoint.log.total.toFixed(2)),
+ 'Logs Cost ($)': parseFloat(dataPoint.log.cost.toFixed(2)),
+ }));
+
+ // Add totals row
+ csvData.push({
+ Date: 'Total',
+ 'Metrics Vol (Mn samples)': parseFloat(totals.metric.total.toFixed(2)),
+ 'Metrics Cost ($)': parseFloat(totals.metric.cost.toFixed(2)),
+ 'Traces Vol (GBs)': parseFloat(totals.trace.total.toFixed(2)),
+ 'Traces Cost ($)': parseFloat(totals.trace.cost.toFixed(2)),
+ 'Logs Vol (GBs)': parseFloat(totals.log.total.toFixed(2)),
+ 'Logs Cost ($)': parseFloat(totals.log.cost.toFixed(2)),
+ });
+
+ return csvData;
+};
+
+export default generateCsvData;
diff --git a/frontend/src/container/BillingContainer/BillingUsageGraph/utils.ts b/frontend/src/container/BillingContainer/BillingUsageGraph/utils.ts
index d40c8a60978..5123d593292 100644
--- a/frontend/src/container/BillingContainer/BillingUsageGraph/utils.ts
+++ b/frontend/src/container/BillingContainer/BillingUsageGraph/utils.ts
@@ -1,6 +1,12 @@
+import { UsageResponsePayloadProps } from 'api/billing/getUsage';
+import dayjs from 'dayjs';
+import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { isEmpty, isNull } from 'lodash-es';
+import { unparse } from 'papaparse';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
+import generateCsvData, { QuantityData } from './generateCsvData';
+
export const convertDataToMetricRangePayload = (
data: any,
): MetricRangePayloadProps => {
@@ -58,10 +64,7 @@ export const convertDataToMetricRangePayload = (
};
};
-export function fillMissingValuesForQuantities(
- data: any,
- timestampArray: number[],
-): MetricRangePayloadProps {
+export function quantityDataArr(data: any, timestampArray: number[]): any[] {
const { result } = data.data;
const transformedResultArr: any[] = [];
@@ -76,6 +79,14 @@ export function fillMissingValuesForQuantities(
);
transformedResultArr.push({ ...item, quantity: quantityArray });
});
+ return transformedResultArr;
+}
+
+export function fillMissingValuesForQuantities(
+ data: any,
+ timestampArray: number[],
+): MetricRangePayloadProps {
+ const transformedResultArr = quantityDataArr(data, timestampArray);
return {
data: {
@@ -85,3 +96,36 @@ export function fillMissingValuesForQuantities(
},
};
}
+
+const formatDate = (timestamp: number): string =>
+ dayjs.unix(timestamp).format('MM/DD/YYYY');
+
+export function csvFileName(csvData: QuantityData[]): string {
+ if (!csvData.length) {
+ return `billing-usage.csv`;
+ }
+
+ const { values } = csvData[0];
+
+ const timestamps = values.map((item) => item[0]);
+ const startDate = formatDate(Math.min(...timestamps));
+ const endDate = formatDate(Math.max(...timestamps));
+
+ return `billing_usage_(${startDate}-${endDate}).csv`;
+}
+
+export function prepareCsvData(
+ data: Partial,
+): {
+ csvData: string;
+ fileName: string;
+} {
+ const graphCompatibleData = convertDataToMetricRangePayload(data);
+ const chartData = getUPlotChartData(graphCompatibleData);
+ const quantityMapArr = quantityDataArr(graphCompatibleData, chartData[0]);
+
+ return {
+ csvData: unparse(generateCsvData(quantityMapArr)),
+ fileName: csvFileName(quantityMapArr),
+ };
+}