Skip to content

Commit cb3f938

Browse files
authored
feat: show warning if a metric for a bucket is missing (#5369)
* feat: show warning if a metric for a bucket is missing
1 parent 50c451e commit cb3f938

11 files changed

Lines changed: 164 additions & 6 deletions

File tree

static/js/publisher/components/PublisherMetrics/PublisherMetrics.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ function PublisherMetrics({ snaps }: { snaps: ISnap[] }): React.JSX.Element {
1616
}
1717
}, [metricsData]);
1818

19+
const daysWithoutDataExist =
20+
metricsData && metricsData.daysWithoutData.length > 0;
1921
return (
2022
<Strip
2123
className="u-no-padding--top"
@@ -28,6 +30,12 @@ function PublisherMetrics({ snaps }: { snaps: ISnap[] }): React.JSX.Element {
2830
size={12}
2931
className="snap-installs-container snapcraft-metrics__graph snapcraft-metrics__active-devices"
3032
>
33+
{daysWithoutDataExist && (
34+
<Notification severity="caution">
35+
Metrics for the most recent days may be incomplete or missing.
36+
They will be updated and accurate within a few hours.
37+
</Notification>
38+
)}
3139
<svg width="100%" height="240"></svg>
3240
{metricsData && metricsData.buckets.length === 0 && (
3341
<div className="p-snap-list__metrics-empty-message">

static/js/publisher/hooks/__tests__/useActiveDeviceMetrics.test.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ describe("useActiveDeviceMetrics", () => {
5555
},
5656
],
5757
},
58+
days_without_data: [],
5859
latest_active_devices: 4,
5960
total_page_num: 1,
6061
}),
@@ -88,6 +89,7 @@ describe("useActiveDeviceMetrics", () => {
8889
],
8990
series: [{ name: "1.0", values: [5, 5, 0, 4, 4] }],
9091
},
92+
daysWithoutData: [],
9193
});
9294
(global.fetch as Mock).mockRestore();
9395
});
@@ -113,6 +115,7 @@ describe("useActiveDeviceMetrics", () => {
113115
},
114116
],
115117
},
118+
days_without_data: [],
116119
latest_active_devices: 4,
117120
total_page_num: 1,
118121
}),

static/js/publisher/hooks/useActiveDeviceMetrics.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ function useActiveDeviceMetrics({
7777
const buckets = [];
7878
const series = new Map();
7979

80+
const daysWithoutData = [];
81+
8082
let seriesThatAreAddedBefore = 0;
8183

8284
for (const result of results.reverse()) {
@@ -110,6 +112,10 @@ function useActiveDeviceMetrics({
110112
}
111113

112114
seriesThatAreAddedBefore += activeDeviceBuckets.length;
115+
116+
if (data.days_without_data) {
117+
daysWithoutData.push(...data.days_without_data);
118+
}
113119
}
114120

115121
const resultArray = Array.from(series.entries()).map(([key, value]) => ({
@@ -122,6 +128,7 @@ function useActiveDeviceMetrics({
122128
buckets,
123129
series: resultArray,
124130
},
131+
daysWithoutData,
125132
};
126133
};
127134

static/js/publisher/hooks/useFetchPublishedSnapMetrics.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ type Series = {
99
type SnapMetrics = {
1010
series: Array<Series>;
1111
buckets: Array<string>;
12+
daysWithoutData: Array<string>;
1213
};
1314

1415
function useFetchPublishedSnapMetrics(snaps: ISnap[]) {
@@ -40,6 +41,7 @@ function useFetchPublishedSnapMetrics(snaps: ISnap[]) {
4041
const metrics: SnapMetrics = {
4142
series: [],
4243
buckets: data.buckets,
44+
daysWithoutData: data.days_without_data,
4345
};
4446

4547
data.snaps.forEach((snap: { series: Array<Series>; name: string }) => {

static/js/publisher/pages/Metrics/ActiveDeviceMetrics.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { useParams, useSearchParams } from "react-router-dom";
2-
import { Row, Col, Spinner, CodeSnippet } from "@canonical/react-components";
2+
import {
3+
Row,
4+
Col,
5+
Spinner,
6+
CodeSnippet,
7+
Notification,
8+
} from "@canonical/react-components";
39

410
import { useEffect } from "react";
511
import { renderActiveDevicesMetrics } from "./metrics/metrics";
@@ -55,10 +61,17 @@ function ActiveDeviceMetrics({
5561
return searchParams;
5662
});
5763
};
64+
const daysWithoutDataExist = data && data.daysWithoutData.length > 0;
5865

5966
return (
6067
<section className={`p-strip is-shallow ${isEmpty ? "is-empty" : ""}`}>
6168
<Row>
69+
{daysWithoutDataExist && (
70+
<Notification severity="caution">
71+
Metrics for the most recent days may be incomplete or missing. They
72+
will be updated and accurate within a few hours.
73+
</Notification>
74+
)}
6275
<Col size={12} key="activeServices">
6376
<h4 className="u-float-left">Weekly active devices</h4>
6477
<div className="p-heading--4 u-float-right u-no-margin--top">

static/js/publisher/pages/Metrics/__tests__/ActiveDeviceMetrics.test.tsx

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,9 @@ describe("ActiveDeviceMetrics", () => {
6262
}
6363
return {
6464
status: "success",
65-
data: {},
65+
data: {
66+
daysWithoutData: [],
67+
},
6668
};
6769
});
6870

@@ -73,6 +75,11 @@ describe("ActiveDeviceMetrics", () => {
7375
expect(screen.getByText("5")).toBeInTheDocument();
7476
expect(screen.getByText("Past 30 days")).toBeInTheDocument();
7577
expect(screen.getByText("By version")).toBeInTheDocument();
78+
expect(
79+
screen.queryByText(
80+
"Metrics for the most recent days may be incomplete or missing. They will be updated and accurate within a few hours.",
81+
),
82+
).toBeNull();
7683
});
7784
});
7885

@@ -97,13 +104,16 @@ describe("ActiveDeviceMetrics", () => {
97104
buckets: [],
98105
name: "annotations",
99106
series: [],
107+
daysWithoutData: [],
100108
},
101109
};
102110
}
103111
}
104112
return {
105113
status: "success",
106-
data: {},
114+
data: {
115+
daysWithoutData: [],
116+
},
107117
};
108118
});
109119

@@ -137,13 +147,16 @@ describe("ActiveDeviceMetrics", () => {
137147
buckets: [],
138148
name: "annotations",
139149
series: [],
150+
daysWithoutData: [],
140151
},
141152
};
142153
}
143154
}
144155
return {
145156
status: "success",
146-
data: {},
157+
data: {
158+
daysWithoutData: [],
159+
},
147160
};
148161
});
149162

@@ -191,4 +204,51 @@ describe("ActiveDeviceMetrics", () => {
191204
expect(screen.getByText("No data found.")).toBeInTheDocument();
192205
});
193206
});
207+
208+
test("renders the warning", async () => {
209+
// @ts-expect-error mocks
210+
useQuery.mockImplementation((params) => {
211+
if (params) {
212+
if (params.queryKey[0] === "activeDeviceMetrics") {
213+
const mock = {
214+
...mockActiveDeviceMetrics,
215+
daysWithoutData: ["2024-08-27"],
216+
};
217+
return {
218+
status: "success",
219+
data: mock,
220+
};
221+
} else if (params.queryKey[0] === "latestActiveDevicesMetric") {
222+
return {
223+
status: "success",
224+
data: 5,
225+
};
226+
} else {
227+
return {
228+
status: "success",
229+
data: {
230+
buckets: [],
231+
name: "annotations",
232+
series: [],
233+
daysWithoutData: [],
234+
},
235+
};
236+
}
237+
}
238+
return {
239+
status: "success",
240+
data: {},
241+
};
242+
});
243+
244+
renderComponent(false);
245+
246+
await waitFor(() => {
247+
expect(
248+
screen.getByText(
249+
"Metrics for the most recent days may be incomplete or missing. They will be updated and accurate within a few hours.",
250+
),
251+
).toBeInTheDocument();
252+
});
253+
});
194254
});

static/js/publisher/pages/Metrics/__tests__/Metrics.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const handlers = [
6060
name: "weekly_installed_base_by_version",
6161
series: [],
6262
},
63+
days_without_data: [],
6364
latest_archive_devices: 0,
6465
total_page_num: 1,
6566
});

static/js/publisher/test-utils/mockActiveDeviceMetrics.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,6 @@ export const mockActiveDeviceMetrics = {
4444
},
4545
],
4646
},
47+
daysWithoutData: [],
4748
latest_active_devices: 5,
4849
};

tests/publisher/tests_account_snaps_metrics.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def test_metrics(self):
5858

5959
expected_response = {
6060
"buckets": ["2018-04-13", "2018-04-20"],
61+
"days_without_data": [],
6162
"snaps": [
6263
{
6364
"id": "1",
@@ -113,3 +114,50 @@ def test_metrics_bad_id_payload(self):
113114

114115
self.assertEqual(500, response.status_code)
115116
self.assertEqual(expected_response, response.json)
117+
118+
@responses.activate
119+
def test_metrics_with_empty_data(self):
120+
metrics_payload = {
121+
"metrics": [
122+
{
123+
"snap_id": "1",
124+
"status": "OK",
125+
"series": [
126+
{"values": [0, 3, None], "name": "new"},
127+
{"values": [2, 3, None], "name": "lost"},
128+
{"values": [9, 6, None], "name": "continued"},
129+
],
130+
"buckets": ["2018-04-13", "2018-04-20", "2018-04-21"],
131+
},
132+
]
133+
}
134+
135+
responses.add(
136+
responses.POST, self.api_url, json=metrics_payload, status=200
137+
)
138+
139+
payload = {"1": "test1"}
140+
headers = {"content-type": "application/json"}
141+
response = self.client.post(
142+
self.endpoint_url, data=json.dumps(payload), headers=headers
143+
)
144+
145+
expected_response = {
146+
"buckets": ["2018-04-13", "2018-04-20", "2018-04-21"],
147+
"days_without_data": ["2018-04-21"],
148+
"snaps": [
149+
{
150+
"id": "1",
151+
"name": None,
152+
"series": [
153+
{"name": "new", "values": [0, 3, None]},
154+
{"name": "lost", "values": [2, 3, None]},
155+
{"name": "continued", "values": [9, 6, None]},
156+
],
157+
}
158+
],
159+
}
160+
161+
self.assertEqual(200, response.status_code)
162+
self.assertEqual(expected_response, response.json)
163+
self.assertEqual(1, len(responses.calls))

webapp/metrics/helper.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,20 @@ def build_snap_installs_metrics_query(snaps, get_filter=get_filter):
152152
return metrics_query
153153

154154

155+
def get_days_without_data(metrics_response):
156+
days_without_data = set()
157+
for metric in metrics_response["metrics"]:
158+
if metric["status"] == "OK":
159+
for series in metric["series"]:
160+
none_indexes = [
161+
i for i, val in enumerate(series["values"]) if val is None
162+
]
163+
days_without_data.update(
164+
metric["buckets"][i] for i in none_indexes
165+
)
166+
return list(days_without_data)
167+
168+
155169
def transform_metrics(metrics, metrics_response, snaps):
156170
"""Transforms an API response from the publisher metrics
157171
@@ -172,7 +186,7 @@ def transform_metrics(metrics, metrics_response, snaps):
172186
{"id": snap_id, "name": snap_name, "series": metric["series"]}
173187
)
174188
metrics["buckets"] = metric["buckets"]
175-
189+
metrics["days_without_data"] = get_days_without_data(metrics_response)
176190
return metrics
177191

178192

0 commit comments

Comments
 (0)