Skip to content

Commit 123bea0

Browse files
committed
Improve error handling in route handlers
1 parent e87b6a0 commit 123bea0

File tree

14 files changed

+358
-55
lines changed

14 files changed

+358
-55
lines changed
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import React, { useEffect, useState, lazy, useMemo } from "react";
2+
import type { ChartDataPoint, ChartData } from "../../types/data";
3+
import type { AxisOptions } from "react-charts";
4+
import { useTheme } from "payload/dist/admin/components/utilities/Theme";
5+
6+
type Props = {};
7+
8+
const ChartComponent = lazy(() =>
9+
import("react-charts").then((module) => {
10+
return { default: module.Chart };
11+
})
12+
);
13+
14+
const GlobalViewsChart: React.FC<Props> = ({}) => {
15+
const [chartData, setChartData] = useState<ChartData>([]);
16+
const [isLoading, setIsLoading] = useState<boolean>(true);
17+
const theme = useTheme();
18+
19+
useEffect(() => {
20+
const getChartData = fetch(`/api/analytics/globalChart`, {
21+
method: "post",
22+
credentials: "include",
23+
headers: {
24+
Accept: "application/json",
25+
"Content-Type": "application/json",
26+
},
27+
body: JSON.stringify({
28+
timeframe: "30d",
29+
metrics: ["views"],
30+
}),
31+
}).then((response) => response.json());
32+
33+
getChartData.then((data: ChartData) => {
34+
setChartData(data);
35+
setIsLoading(false);
36+
});
37+
}, []);
38+
39+
const timeframeIndicator = useMemo(() => {
40+
return new Date().toLocaleString("default", { month: "long" });
41+
}, []);
42+
43+
const chartLabel = useMemo(() => {
44+
return "Sitewide views";
45+
}, []);
46+
47+
const primaryAxis = React.useMemo<AxisOptions<ChartDataPoint>>(() => {
48+
return {
49+
getValue: (datum) => datum.timestamp,
50+
show: false,
51+
elementType: "line",
52+
showDatumElements: false,
53+
};
54+
}, []);
55+
56+
const secondaryAxes = React.useMemo<AxisOptions<ChartDataPoint>[]>(
57+
() => [
58+
{
59+
getValue: (datum) => {
60+
return datum.value;
61+
},
62+
elementType: "line",
63+
},
64+
],
65+
[]
66+
);
67+
68+
return (
69+
<section
70+
style={{
71+
marginBottom: "1.5rem",
72+
}}
73+
>
74+
{chartData?.length && chartData.length > 0 ? (
75+
<>
76+
<h1 style={{ fontSize: "1.25rem", marginBottom: "0.5rem" }}>
77+
{chartLabel} ({timeframeIndicator})
78+
</h1>
79+
<div style={{ minHeight: "200px", position: "relative" }}>
80+
<ChartComponent
81+
options={{
82+
data: chartData,
83+
dark: theme.theme === "dark",
84+
initialHeight: 220,
85+
tooltip: false,
86+
/* @ts-ignore */
87+
primaryAxis,
88+
/* @ts-ignore */
89+
secondaryAxes,
90+
}}
91+
/>
92+
</div>
93+
</>
94+
) : isLoading ? (
95+
<> Loading...</>
96+
) : (
97+
<div>No data found for {chartLabel}.</div>
98+
)}
99+
</section>
100+
);
101+
};
102+
103+
export default GlobalViewsChart;

src/components/Reports/TopPages.tsx

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,79 @@ import React, {
77
useMemo,
88
} from "react";
99

10+
import type { ReportData } from "../../types/data";
1011
interface Props {}
1112

1213
const TopPages: React.FC<Props> = () => {
13-
return <div>hello</div>;
14+
const [data, setData] = useState<ReportData>();
15+
const [isLoading, setIsLoading] = useState<boolean>(true);
16+
17+
/* const { label } = options; */
18+
19+
useEffect(() => {
20+
const getLiveData = fetch(`/api/analytics/report`, {
21+
method: "post",
22+
credentials: "include",
23+
headers: {
24+
Accept: "application/json",
25+
"Content-Type": "application/json",
26+
},
27+
body: JSON.stringify({
28+
metrics: ["views"],
29+
property: "page",
30+
}),
31+
}).then((response) => response.json());
32+
33+
getLiveData.then((data: ReportData) => {
34+
setData(data);
35+
setIsLoading(false);
36+
});
37+
}, []);
38+
39+
return (
40+
<section
41+
style={{
42+
marginBottom: "1.5rem",
43+
border: "1px solid",
44+
borderColor: "var(--theme-elevation-100)",
45+
padding: "0.5rem",
46+
width: "100%",
47+
}}
48+
>
49+
<h1 style={{ fontSize: "1.25rem", marginBottom: "0.75rem" }}>
50+
{"Top pages"}
51+
</h1>
52+
<div>
53+
{isLoading ? (
54+
<>Loading...</>
55+
) : (
56+
<ul style={{ margin: "0", listStyle: "none", padding: "0" }}>
57+
{data?.map((item, itemIndex) => {
58+
const property = Object.keys(item)[0];
59+
60+
return (
61+
<li
62+
style={{
63+
display: "flex",
64+
justifyContent: "space-between",
65+
width: "100%",
66+
}}
67+
key={itemIndex}
68+
>
69+
<div style={{ fontWeight: "700" }}>{item[property]}</div>
70+
{item.values.map((value, valueIndex) => {
71+
const valueKey = Object.keys(value)[0];
72+
73+
return <div key={valueIndex}>{value[valueKey]}</div>;
74+
})}
75+
</li>
76+
);
77+
})}
78+
</ul>
79+
)}
80+
</div>
81+
</section>
82+
);
1483
};
1584

1685
export default TopPages;

src/extendWebpackConfig.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ export const extendWebpackConfig =
2121
? existingWebpackConfig.resolve.alias
2222
: {}),
2323
express: mockModulePath,
24-
[path.resolve(__dirname, "./providers/")]: mockModulePath,
25-
[path.resolve(__dirname, "./routes/")]: mockModulePath,
24+
[path.resolve(__dirname, "./providers/plausible/client")]:
25+
mockModulePath,
26+
/* [path.resolve(__dirname, "./routes/")]: mockModulePath, */
2627
},
2728
},
2829
};

src/providers/plausible/client.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import type { PlausibleProvider } from "../../types/providers";
2-
import type { Metrics } from "../../types/widgets";
3-
import { MetricMap } from "./utilities";
2+
import type { Metrics, Properties } from "../../types/widgets";
3+
import { MetricMap, PropertyMap } from "./utilities";
44

55
type ClientOptions = {
66
endpoint: string;
77
timeframe?: string;
88
metrics?: Metrics[];
9+
property?: Properties;
910
};
1011

1112
function client(provider: PlausibleProvider, options: ClientOptions) {
12-
const { endpoint, timeframe, metrics } = options;
13+
const { endpoint, timeframe, metrics, property } = options;
1314
const host = provider.host ?? `https://plausible.io`;
1415
const apiVersion = `v1`; // for future use
1516

@@ -49,15 +50,28 @@ function client(provider: PlausibleProvider, options: ClientOptions) {
4950
url.searchParams.append("period", period());
5051
url.searchParams.append("metrics", String(plausibleMetrics));
5152

53+
if (property) {
54+
const availableProperties = Object.entries(PropertyMap);
55+
56+
const foundMetric = availableProperties.find((mappedProperty) => {
57+
return mappedProperty[0] === property;
58+
});
59+
60+
if (foundMetric) {
61+
url.searchParams.append("property", String(foundMetric[1].value));
62+
}
63+
}
64+
5265
return {
5366
host: host,
5467
baseUrl: baseUrl,
5568
metric: plausibleMetrics,
5669
url: url,
5770
metricsMap: MetricMap,
71+
propertiesMap: PropertyMap,
5872
fetch: async (customUrl?: string) => {
5973
const fetchUrl = customUrl ?? url.toString();
60-
74+
console.log("fetching with", url.toString());
6175
return await fetch(fetchUrl, {
6276
method: "get",
6377
headers: new Headers({

src/providers/plausible/getReportData.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,41 @@ async function getReportData(
99
) {
1010
const plausibleClient = client(provider, {
1111
endpoint: "/stats/breakdown",
12+
metrics: options.metrics,
13+
property: options.property,
1214
});
1315

14-
const data = await plausibleClient.fetch().then((response) => {
16+
const url = plausibleClient.url;
17+
18+
const metricsMap = plausibleClient.metricsMap;
19+
20+
url.searchParams.append("limit", "10");
21+
22+
const data = await plausibleClient.fetch(url.toString()).then((response) => {
1523
return response.json();
1624
});
1725

1826
const processedData: ReportData = data.results.map((item: any) => {
19-
return item;
27+
const matchingProperyKey = Object.keys(item)[0];
28+
29+
return {
30+
[options.property]: item[matchingProperyKey],
31+
values: Object.keys(item)
32+
.map((value) => {
33+
const matchingMetric = Object.entries(metricsMap).find(
34+
([key, metricValue]) => {
35+
return value === metricValue.value;
36+
}
37+
);
38+
39+
if (matchingMetric) {
40+
return {
41+
[matchingMetric[0]]: item[value],
42+
};
43+
}
44+
})
45+
.filter((filterItem) => Boolean(filterItem)),
46+
};
2047
});
2148

2249
return processedData;

src/providers/plausible/utilities.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { MetricsMap } from "../../types/data";
1+
import { MetricsMap, PropertiesMap } from "../../types/data";
22

33
export const MetricMap: MetricsMap = {
44
views: {
@@ -10,3 +10,26 @@ export const MetricMap: MetricsMap = {
1010
sessionDuration: { label: "Avg. duration", value: "visit_duration" },
1111
sessions: { label: "Sessions", value: "visits" },
1212
};
13+
14+
export const PropertyMap: PropertiesMap = {
15+
page: {
16+
label: "Pages",
17+
value: "event:page",
18+
},
19+
country: {
20+
label: "Pages",
21+
value: "event:page",
22+
},
23+
entryPoint: {
24+
label: "Pages",
25+
value: "event:page",
26+
},
27+
exitPoint: {
28+
label: "Pages",
29+
value: "event:page",
30+
},
31+
source: {
32+
label: "Pages",
33+
value: "event:page",
34+
},
35+
};

src/routes/getGlobalAggregate/handler.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,30 @@
11
import { Endpoint } from "payload/config";
22
import { ApiProvider } from "../../providers";
3-
import payload from "payload";
43

54
const handler = (provider: ApiProvider) => {
65
const handler: Endpoint["handler"] = async (req, res, next) => {
6+
const { payload } = req;
7+
const { timeframe, metrics } = req.body;
8+
9+
if (!metrics) {
10+
payload.logger.error("📊 Analytics API: Missing metrics argument.");
11+
res.status(500).send("Missing metrics argument.");
12+
return next();
13+
}
14+
715
try {
8-
const { timeframe, metrics } = req.body;
9-
const data = await provider.getGlobalAggregateData({
10-
timeframe,
11-
metrics,
12-
});
16+
const data = await provider
17+
.getGlobalAggregateData({
18+
timeframe,
19+
metrics,
20+
})
21+
.catch((error) => payload.logger.error(error));
22+
1323
res.status(200).send(data);
1424
} catch (error) {
15-
payload.logger.error(payload);
16-
res.sendStatus(500);
25+
payload.logger.error(error);
26+
res.status(500).send(`📊 Analytics API: ${error}`);
27+
return next();
1728
}
1829
};
1930

src/routes/getGlobalChart/handler.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
11
import { Endpoint } from "payload/config";
22
import { ApiProvider } from "../../providers";
3-
import payload from "payload";
43

54
const handler = (provider: ApiProvider) => {
65
const handler: Endpoint["handler"] = async (req, res, next) => {
7-
try {
8-
const { timeframe, metrics } = req.body;
6+
const { payload } = req;
7+
const { timeframe, metrics } = req.body;
8+
9+
if (!metrics) {
10+
payload.logger.error("📊 Analytics API: Missing metrics argument.");
11+
res.status(500).send("Missing metrics argument.");
12+
return next();
13+
}
914

15+
try {
1016
const data = await provider.getGlobalChartData({
1117
timeframe: timeframe,
1218
metrics: metrics,
1319
});
20+
1421
res.status(200).send(data);
1522
} catch (error) {
1623
payload.logger.error(error);
17-
res.sendStatus(500);
24+
res.status(500).send(`📊 Analytics API: ${error}`);
25+
return next();
1826
}
1927
};
2028

0 commit comments

Comments
 (0)