Skip to content

Commit

Permalink
Request a metric to be measured ASAP.
Browse files Browse the repository at this point in the history
Allow for requesting a metric to be measured as soon as possible.

Closes #920.
  • Loading branch information
fniessink committed Jun 24, 2024
1 parent c804b3b commit 109af8d
Show file tree
Hide file tree
Showing 15 changed files with 247 additions and 153 deletions.
14 changes: 12 additions & 2 deletions components/collector/src/database/measurements.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,11 @@ def create_measurement(database: Database, measurement_data: dict) -> None:
measurement.copy_entity_first_seen_timestamps(latest_successful)
measurement.copy_entity_user_data(latest if latest_successful is None else latest_successful)
measurement.update_measurement() # Update the scales so we can compare the two measurements
if measurement.equals(latest):
# If the new measurement is equal to the previous one, merge them together
if measurement.equals(latest) and no_measurement_requested_after(latest):
# If the new measurement is equal to the latest one and no measurement update was requested, merge the
# two measurements together. Don't merge the measurements together when a measurement was requested,
# even if the new measurement value did not change, so that the frontend gets a new measurement count
# via the number of measurements server-sent events endpoint and knows the requested measurement was done.
update_measurement_end(database, latest["_id"])
return
insert_new_measurement(database, measurement)
Expand All @@ -38,3 +41,10 @@ def create_measurement(database: Database, measurement_data: dict) -> None:
def update_measurement_end(database: Database, measurement_id: MeasurementId) -> None: # pragma: no feature-test-cover
"""Set the end date and time of the measurement to the current date and time."""
database.measurements.update_one(filter={"_id": measurement_id}, update={"$set": {"end": iso_timestamp()}})


def no_measurement_requested_after(measurement: Measurement) -> bool:
"""Return whether a measurement was requested later than the end of this measurement."""
if measurement_request_timestamp := measurement.metric.get("measurement_requested"):
return bool(measurement_request_timestamp < measurement["end"])
return True
9 changes: 9 additions & 0 deletions components/collector/tests/database/test_measurements.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,12 @@ def test_copy_first_seen_timestamps(self):
"2023-07-18",
next(self.database.measurements.find())["sources"][0]["entities"][0]["first_seen"],
)

def test_create_measurement_after_measurement_was_requested(self):
"""Test that a new measurement is added after a measurement request, even if the measurement is unchanged."""
self.database["reports"].insert_one(create_report(report_uuid=REPORT_ID))
create_measurement(self.database, self.measurement_data())
self.database["reports"].update_one({"report_uuid": REPORT_ID}, {"$set": {"last": False}})
self.database["reports"].insert_one(create_report(report_uuid=REPORT_ID, measurement_requested="3000-01-01"))
create_measurement(self.database, self.measurement_data())
self.assertEqual(2, len(list(self.database.measurements.find())))
4 changes: 4 additions & 0 deletions components/collector/tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def create_report(title: str = "Title", report_uuid: str = "report1", **kwargs)
metric_id: MetricId = METRIC_ID
metric_type = "dependencies"
source_type = "pip"
measurement_requested = None

for key, value in kwargs.items():
match key:
Expand All @@ -32,6 +33,8 @@ def create_report(title: str = "Title", report_uuid: str = "report1", **kwargs)
metric_type = value
case "source_type":
source_type = value
case "measurement_requested":
measurement_requested = value
case _:
raise ValueError

Expand All @@ -53,6 +56,7 @@ def create_report(title: str = "Title", report_uuid: str = "report1", **kwargs)
"parameters": {"url": "https://url", "password": "password"},
},
},
"measurement_requested": measurement_requested,
},
}

Expand Down
16 changes: 14 additions & 2 deletions components/frontend/src/measurement/MeasurementValue.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ import { Icon } from "semantic-ui-react"
import { DataModel } from "../context/DataModel"
import { Label, Popup } from "../semantic_ui_react_wrappers"
import { datePropType, metricPropType } from "../sharedPropTypes"
import { formatMetricValue, getMetricScale, getMetricValue, MILLISECONDS_PER_HOUR } from "../utils"
import {
formatMetricValue,
getMetricScale,
getMetricValue,
isMeasurementRequested,
MILLISECONDS_PER_HOUR,
} from "../utils"
import { TimeAgoWithDate } from "../widgets/TimeAgoWithDate"
import { WarningMessage } from "../widgets/WarningMessage"

Expand Down Expand Up @@ -42,8 +48,9 @@ export function MeasurementValue({ metric, reportDate }) {
const now = reportDate ?? new Date()
const stale = now - end > MILLISECONDS_PER_HOUR // No new measurement for more than one hour means something is wrong
const outdated = metric.latest_measurement.outdated ?? false
const requested = isMeasurementRequested(metric)
return (
<Popup trigger={measurementValueLabel(stale, outdated, value)} flowing hoverable>
<Popup trigger={measurementValueLabel(stale, outdated || requested, value)} flowing hoverable>
<WarningMessage
showIf={stale}
header="This metric was not recently measured"
Expand All @@ -54,6 +61,11 @@ export function MeasurementValue({ metric, reportDate }) {
header="Latest measurement out of date"
content="The source configuration of this metric was changed after the latest measurement."
/>
<WarningMessage
showIf={requested}
header="Measurement requested"
content="An update of the latest measurement was requested by a user."
/>
<TimeAgoWithDate date={metric.latest_measurement.end}>
{metric.status ? "The metric was last measured" : "Last measurement attempt"}
</TimeAgoWithDate>
Expand Down
220 changes: 90 additions & 130 deletions components/frontend/src/measurement/MeasurementValue.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,60 +4,56 @@ import userEvent from "@testing-library/user-event"
import { DataModel } from "../context/DataModel"
import { MeasurementValue } from "./MeasurementValue"

it("renders the value", () => {
function renderMeasurementValue({
latest_measurement = {},
measurement_requested = null,
reportDate = null,
scale = "count",
status = null,
type = "violations",
unit = null,
} = {}) {
render(
<DataModel.Provider value={{ metrics: { violations: { unit: "violations" } } }}>
<MeasurementValue
metric={{
type: "violations",
scale: "count",
unit: null,
latest_measurement: { count: { value: "42" } },
latest_measurement: latest_measurement,
measurement_requested: measurement_requested,
scale: scale,
status: status,
type: type,
unit: unit,
}}
reportDate={reportDate}
/>
</DataModel.Provider>,
)
}

it("renders the value", () => {
renderMeasurementValue({ latest_measurement: { count: { value: "42" } } })
expect(screen.getAllByText(/42/).length).toBe(1)
})

it("renders an unkown value", () => {
render(
<DataModel.Provider value={{ metrics: { violations: { unit: "violations" } } }}>
<MeasurementValue
metric={{
type: "violations",
scale: "count",
unit: null,
latest_measurement: { count: { value: null } },
}}
/>
</DataModel.Provider>,
)
renderMeasurementValue({ latest_measurement: { count: { value: null } } })
expect(screen.getAllByText(/\?/).length).toBe(1)
})

it("renders a value that has not been measured yet", () => {
render(
<DataModel.Provider value={{ metrics: { violations: { unit: "violations" } } }}>
<MeasurementValue metric={{ type: "violations", scale: "count", unit: null, latest_measurement: {} }} />
</DataModel.Provider>,
)
renderMeasurementValue()
expect(screen.getAllByText(/\?/).length).toBe(1)
})

it("renders an outdated value", async () => {
render(
<DataModel.Provider value={{ metrics: { violations: { unit: "violations" } } }}>
<MeasurementValue
metric={{
type: "violations",
scale: "count",
unit: null,
latest_measurement: { count: { value: 1 }, outdated: true },
}}
/>
</DataModel.Provider>,
)
renderMeasurementValue({
latest_measurement: {
count: { value: 1 },
outdated: true,
start: new Date().toISOString(),
end: new Date().toISOString(),
},
})
const measurementValue = screen.getByText(/1/)
expect(measurementValue.className).toContain("yellow")
expect(measurementValue.children[0].className).toContain("loading")
Expand All @@ -70,112 +66,86 @@ it("renders an outdated value", async () => {
})
})

it("renders a value for which a measurement was requested", async () => {
const now = new Date().toISOString()
renderMeasurementValue({
latest_measurement: { count: { value: 1 }, start: now, end: now },
measurement_requested: now,
})
const measurementValue = screen.getByText(/1/)
expect(measurementValue.className).toContain("yellow")
expect(measurementValue.children[0].className).toContain("loading")
await userEvent.hover(measurementValue)
await waitFor(() => {
expect(screen.queryByText(/Measurement requested/)).not.toBe(null)
expect(screen.queryByText(/An update of the latest measurement was requested by a user/)).not.toBe(null)
})
})

it("renders a value for which a measurement was requested, but which is now up to date", async () => {
const now = new Date().toISOString()
renderMeasurementValue({
latest_measurement: { count: { value: 1 }, start: now, end: now },
measurement_requested: "2024-01-01T00:00:00",
})
const measurementValue = screen.getByText(/1/)
expect(measurementValue.className).not.toContain("yellow")
await userEvent.hover(measurementValue)
await waitFor(() => {
expect(screen.queryByText(/Measurement requested/)).toBe(null)
expect(screen.queryByText(/An update of the latest measurement was requested by a user/)).toBe(null)
})
})

it("renders a minutes value", () => {
render(
<DataModel.Provider value={{ metrics: { duration: { unit: "minutes" } } }}>
<MeasurementValue
metric={{
type: "duration",
scale: "count",
unit: null,
latest_measurement: { count: { value: "42" } },
}}
/>
</DataModel.Provider>,
)
renderMeasurementValue({
type: "duration",
latest_measurement: { count: { value: "42" } },
})
expect(screen.getAllByText(/42/).length).toBe(1)
})

it("renders an unknown minutes value", () => {
render(
<DataModel.Provider value={{ metrics: { duration: { unit: "minutes" } } }}>
<MeasurementValue
metric={{
type: "duration",
scale: "count",
unit: null,
latest_measurement: { count: { value: null } },
}}
/>
</DataModel.Provider>,
)
renderMeasurementValue({
type: "duration",
latest_measurement: { count: { value: null } },
})
expect(screen.getAllByText(/\?/).length).toBe(1)
})

it("renders a minutes percentage", () => {
render(
<DataModel.Provider value={{ metrics: { duration: { unit: "minutes" } } }}>
<MeasurementValue
metric={{
type: "duration",
scale: "percentage",
unit: null,
latest_measurement: { percentage: { value: "42" } },
}}
/>
</DataModel.Provider>,
)
renderMeasurementValue({
type: "duration",
scale: "percentage",
latest_measurement: { percentage: { value: "42" } },
})
expect(screen.getAllByText(/42%/).length).toBe(1)
})

it("renders an unknown minutes percentage", () => {
render(
<DataModel.Provider value={{ metrics: { duration: { unit: "minutes" } } }}>
<MeasurementValue
metric={{
type: "duration",
scale: "percentage",
unit: null,
latest_measurement: { percentage: { value: null } },
}}
/>
</DataModel.Provider>,
)
renderMeasurementValue({
type: "duration",
scale: "percentage",
latest_measurement: { percentage: { value: null } },
})
expect(screen.getAllByText(/\?%/).length).toBe(1)
})

it("shows when the metric was last measured", async () => {
render(
<DataModel.Provider value={{ metrics: { violations: { unit: "violations" } } }}>
<MeasurementValue
metric={{
status: "target_met",
type: "violations",
scale: "count",
unit: null,
latest_measurement: {
start: "2022-01-16T00:31:00",
end: "2022-01-16T00:51:00",
count: { value: "42" },
},
}}
/>
</DataModel.Provider>,
)
renderMeasurementValue({
status: "target_met",
latest_measurement: { start: "2022-01-16T00:31:00", end: "2022-01-16T00:51:00", count: { value: "42" } },
})
await userEvent.hover(screen.queryByText(/42/))
await waitFor(() => {
expect(screen.queryByText(/The metric was last measured/)).not.toBe(null)
})
})

it("shows when the last measurement attempt was", async () => {
render(
<DataModel.Provider value={{ metrics: { violations: { unit: "violations" } } }}>
<MeasurementValue
metric={{
status: null,
type: "violations",
scale: "count",
unit: null,
latest_measurement: {
start: "2022-01-16T00:31:00",
end: "2022-01-16T00:51:00",
count: { value: null },
},
}}
/>
</DataModel.Provider>,
)
renderMeasurementValue({
latest_measurement: { start: "2022-01-16T00:31:00", end: "2022-01-16T00:51:00", count: { value: null } },
})
await userEvent.hover(screen.queryByText(/\?/))
await waitFor(() => {
expect(screen.queryByText(/This metric was not recently measured/)).not.toBe(null)
Expand All @@ -185,20 +155,10 @@ it("shows when the last measurement attempt was", async () => {

it("does not show an error message for past measurements that were recently measured at the time", async () => {
const reportDate = new Date("2022-01-16T01:00:00")
render(
<DataModel.Provider value={{ metrics: { violations: { unit: "violations" } } }}>
<MeasurementValue
metric={{
type: "violations",
latest_measurement: {
start: "2022-01-16T00:31:00",
end: "2022-01-16T00:51:00",
},
}}
reportDate={reportDate}
/>
</DataModel.Provider>,
)
renderMeasurementValue({
latest_measurement: { start: "2022-01-16T00:31:00", end: "2022-01-16T00:51:00" },
reportDate: reportDate,
})
await userEvent.hover(screen.queryByText(/\?/))
await waitFor(() => {
expect(screen.queryByText(/This metric was not recently measured/)).toBe(null)
Expand Down
Loading

0 comments on commit 109af8d

Please sign in to comment.