mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-07 02:22:49 +08:00
## Background Currently you can "dismiss" problem checks on the dashboard, but if the problem persists it will show up again on the next reload, which is confusing. There was previously some discussion about adding a feature to "snooze" problem checks, but I think even with that it remains a bit too opaque. You'd need to spelunk around in the console to try and figure out what is going on. ## What is this change? This PR does a couple of things: ### 1. Replace Dismiss with Ignore Hitting ignore will prevent the problem check from creating new admin notices until it has been unignored from the new problem checks page. **Screenshot** <img width="395" height="61" alt="Screenshot 2026-04-05 at 4 37 39 PM" src="https://github.com/user-attachments/assets/4816fd04-046b-441e-9471-c160dd3f82b9" /> ### 2. Add a new problem check page This page provides a list of problem checks with information on whether they are passing or failing, and when they were last run. You can also ignore or unignore (watch) problem checks from here. **Screenshot** <img width="600" height="200" alt="Screenshot 2026-04-05 at 4 26 37 PM" src="https://github.com/user-attachments/assets/d8cb2b6a-3f56-409c-97f0-312cb1545654" /> ### 3. Remove the problem check timestamp from the dashboard This timestamp made sense under the previous model, where all checks were run at once and the results cached. With the new model, there's a mix of on-demand and scheduled checks, and having a single timestamp is misleading at best. In practice it's always going to be just the timestamp when you last loaded the dashboard. **Before** <img width="240" height="100" alt="Screenshot 2026-04-05 at 4 30 21 PM" src="https://github.com/user-attachments/assets/1846e024-0042-476e-8b5d-41b6745af75f" /> **After** <img width="210" height="95" alt="Screenshot 2026-04-05 at 4 29 19 PM" src="https://github.com/user-attachments/assets/a39c87c1-c1e3-4621-8219-e3903ba2ada4" />
380 lines
11 KiB
Ruby
380 lines
11 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
RSpec.describe ProblemCheckTracker do
|
|
describe "validations" do
|
|
let(:record) { described_class.new(identifier: "twitter_login") }
|
|
|
|
it { expect(record).to validate_presence_of(:identifier) }
|
|
it { expect(record).to validate_uniqueness_of(:identifier).scoped_to(:target) }
|
|
|
|
it { expect(record).to validate_numericality_of(:blips).is_greater_than_or_equal_to(0) }
|
|
|
|
it { expect(record).to validate_presence_of(:target) }
|
|
end
|
|
|
|
describe "callbacks" do
|
|
describe "before_destroy (silence the alarm)" do
|
|
let(:tracker) do
|
|
ProblemCheckTracker.create!(identifier: "twitter_login", target: ProblemCheck::NO_TARGET)
|
|
end
|
|
|
|
before { tracker.problem! }
|
|
|
|
it "removes any associated admin notices" do
|
|
expect { tracker.destroy }.to change { AdminNotice.count }.by(-1)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe ".[]" do
|
|
before { Fabricate(:problem_check_tracker, identifier: "twitter_login") }
|
|
|
|
context "when the problem check tracker already exists" do
|
|
it { expect(described_class[:twitter_login]).not_to be_new_record }
|
|
end
|
|
|
|
context "when the problem check tracker doesn't exist yet" do
|
|
it { expect(described_class[:facebook_login]).to be_previously_new_record }
|
|
end
|
|
end
|
|
|
|
describe "#check" do
|
|
before do
|
|
Fabricate(:problem_check_tracker, identifier: "twitter_login")
|
|
Fabricate(:problem_check_tracker, identifier: "missing_check")
|
|
end
|
|
|
|
context "when the tracker has a corresponding check" do
|
|
it { expect(described_class[:twitter_login].check.new).to be_a(ProblemCheck) }
|
|
end
|
|
|
|
context "when the checking logic of the tracker has been removed or renamed" do
|
|
it do
|
|
expect { described_class[:missing_check].check }.to change { described_class.count }.by(-1)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#ready_to_run?" do
|
|
let(:problem_tracker) { described_class.new(next_run_at:) }
|
|
|
|
context "when the next run timestamp is not set" do
|
|
let(:next_run_at) { nil }
|
|
|
|
it { expect(problem_tracker).to be_ready_to_run }
|
|
end
|
|
|
|
context "when the next run timestamp is in the past" do
|
|
let(:next_run_at) { 5.minutes.ago }
|
|
|
|
it { expect(problem_tracker).to be_ready_to_run }
|
|
end
|
|
|
|
context "when the next run timestamp is in the future" do
|
|
let(:next_run_at) { 5.minutes.from_now }
|
|
|
|
it { expect(problem_tracker).not_to be_ready_to_run }
|
|
end
|
|
end
|
|
|
|
describe "#failing?" do
|
|
before { freeze_time }
|
|
|
|
let(:problem_tracker) { described_class.new(last_problem_at:, last_run_at:, last_success_at:) }
|
|
|
|
context "when the last run passed" do
|
|
let(:last_run_at) { 1.minute.ago }
|
|
let(:last_success_at) { 1.minute.ago }
|
|
let(:last_problem_at) { 11.minutes.ago }
|
|
|
|
it { expect(problem_tracker).not_to be_failing }
|
|
end
|
|
|
|
context "when the last run had a problem" do
|
|
let(:last_run_at) { 1.minute.ago }
|
|
let(:last_success_at) { 11.minutes.ago }
|
|
let(:last_problem_at) { 1.minute.ago }
|
|
|
|
it { expect(problem_tracker).to be_failing }
|
|
end
|
|
end
|
|
|
|
describe "#passing?" do
|
|
before { freeze_time }
|
|
|
|
let(:problem_tracker) { described_class.new(last_problem_at:, last_run_at:, last_success_at:) }
|
|
|
|
context "when the last run passed" do
|
|
let(:last_run_at) { 1.minute.ago }
|
|
let(:last_success_at) { 1.minute.ago }
|
|
let(:last_problem_at) { 11.minutes.ago }
|
|
|
|
it { expect(problem_tracker).to be_passing }
|
|
end
|
|
|
|
context "when the last run had a problem" do
|
|
let(:last_run_at) { 1.minute.ago }
|
|
let(:last_success_at) { 11.minutes.ago }
|
|
let(:last_problem_at) { 1.minute.ago }
|
|
|
|
it { expect(problem_tracker).not_to be_passing }
|
|
end
|
|
end
|
|
|
|
describe "#ignored?" do
|
|
let(:problem_tracker) { described_class.new(ignored_at:) }
|
|
|
|
context "when the ignored timestamp is set" do
|
|
let(:ignored_at) { 1.day.ago }
|
|
|
|
it { expect(problem_tracker).to be_ignored }
|
|
end
|
|
|
|
context "when the ignored timestamp is not set" do
|
|
let(:ignored_at) { nil }
|
|
|
|
it { expect(problem_tracker).not_to be_ignored }
|
|
end
|
|
end
|
|
|
|
describe "#watched?" do
|
|
let(:problem_tracker) { described_class.new(ignored_at:) }
|
|
|
|
context "when the ignored timestamp is set" do
|
|
let(:ignored_at) { 1.day.ago }
|
|
|
|
it { expect(problem_tracker).not_to be_watched }
|
|
end
|
|
|
|
context "when the ignored timestamp is not set" do
|
|
let(:ignored_at) { nil }
|
|
|
|
it { expect(problem_tracker).to be_watched }
|
|
end
|
|
end
|
|
|
|
describe "#ignore!" do
|
|
let(:problem_tracker) { Fabricate(:problem_check_tracker, ignored_at:) }
|
|
|
|
context "when not currently ignored" do
|
|
let(:ignored_at) { nil }
|
|
|
|
it "sets the ignore timestamp" do
|
|
freeze_time
|
|
|
|
expect { problem_tracker.ignore! }.to change { problem_tracker.ignored_at }.from(nil).to(
|
|
be_within_one_second_of Time.current
|
|
)
|
|
end
|
|
end
|
|
|
|
context "when already ignored" do
|
|
let(:ignored_at) { 1.day.ago }
|
|
|
|
it "does not touch the ignore timestamp" do
|
|
expect { problem_tracker.ignore! }.not_to change { problem_tracker.ignored_at }
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#watch!" do
|
|
let(:problem_tracker) { Fabricate(:problem_check_tracker, ignored_at:) }
|
|
|
|
context "when not currently ignored" do
|
|
let(:ignored_at) { nil }
|
|
|
|
it "does not touch the ignore timestamp" do
|
|
expect { problem_tracker.watch! }.not_to change { problem_tracker.ignored_at }
|
|
end
|
|
end
|
|
|
|
context "when currently ignored" do
|
|
let(:ignored_at) { 1.day.ago }
|
|
|
|
it "clears the ignore timestamp" do
|
|
expect { problem_tracker.watch! }.to change { problem_tracker.ignored_at }.from(
|
|
be_within_one_second_of ignored_at
|
|
).to(nil)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#problem!" do
|
|
let(:problem_tracker) do
|
|
Fabricate(
|
|
:problem_check_tracker,
|
|
identifier: "twitter_login",
|
|
target: "foo",
|
|
ignored_at:,
|
|
**original_attributes,
|
|
)
|
|
end
|
|
|
|
let(:original_attributes) do
|
|
{
|
|
blips:,
|
|
last_problem_at: 1.week.ago,
|
|
last_success_at: 24.hours.ago,
|
|
last_run_at: 24.hours.ago,
|
|
next_run_at: nil,
|
|
}
|
|
end
|
|
|
|
let(:blips) { 0 }
|
|
let(:updated_attributes) { { blips: 1 } }
|
|
let(:ignored_at) { nil }
|
|
|
|
it do
|
|
freeze_time
|
|
|
|
expect { problem_tracker.problem!(next_run_at: 24.hours.from_now) }.to change {
|
|
problem_tracker.attributes
|
|
}.to(hash_including(updated_attributes))
|
|
end
|
|
|
|
context "when the maximum number of blips have been surpassed" do
|
|
let(:blips) { 1 }
|
|
|
|
context "when the check isn't being ignored" do
|
|
let(:ignored_at) { nil }
|
|
|
|
it "sounds the alarm" do
|
|
expect { problem_tracker.problem!(next_run_at: 24.hours.from_now) }.to change {
|
|
AdminNotice.problem.count
|
|
}.by(1)
|
|
end
|
|
end
|
|
|
|
context "when the check is being ignored" do
|
|
let(:ignored_at) { 1.day.ago }
|
|
|
|
it "does not sound the alarm" do
|
|
expect { problem_tracker.problem!(next_run_at: 24.hours.from_now) }.not_to change {
|
|
AdminNotice.problem.count
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when the details of the problem change but the problem remains" do
|
|
let(:blips) { 1 }
|
|
|
|
it "updates the notice" do
|
|
original_details = {
|
|
themes_list:
|
|
"<ul><li><a href=\"/admin/customize/themes/13\">discourse-blank-theme</a></li> <li><a href=\"/admin/customize/themes/31\">Simple Theme</a></li></ul>",
|
|
base_path: "",
|
|
}
|
|
|
|
expect do problem_tracker.problem!(details: original_details) end.to change {
|
|
AdminNotice.problem.count
|
|
}.by(1)
|
|
|
|
admin_notice = AdminNotice.problem.find_by(identifier: "twitter_login")
|
|
|
|
expect(
|
|
admin_notice.details.merge(target: problem_tracker.target).with_indifferent_access,
|
|
).to eq(original_details.merge(target: problem_tracker.target).with_indifferent_access)
|
|
|
|
new_details = {
|
|
themes_list: "<ul><li><a href=\"/admin/customize/themes/31\">Simple Theme</a></li></ul>",
|
|
base_path: "",
|
|
}
|
|
expect do problem_tracker.problem!(details: new_details) end.not_to change {
|
|
AdminNotice.problem.count
|
|
}
|
|
|
|
admin_notice.reload
|
|
|
|
expect(
|
|
admin_notice.details.merge(target: problem_tracker.target).with_indifferent_access,
|
|
).to eq(new_details.merge(target: problem_tracker.target).with_indifferent_access)
|
|
end
|
|
end
|
|
|
|
context "when there's an alarm sounding for multi-target trackers" do
|
|
let(:blips) { 1 }
|
|
|
|
before do
|
|
Fabricate(
|
|
:admin_notice,
|
|
subject: "problem",
|
|
identifier: "twitter_login",
|
|
details: {
|
|
target: target,
|
|
},
|
|
)
|
|
end
|
|
|
|
context "when the alarm is for a different target" do
|
|
let(:target) { "bar" }
|
|
|
|
it "sounds the alarm" do
|
|
expect { problem_tracker.problem!(next_run_at: 24.hours.from_now) }.to change {
|
|
AdminNotice.problem.count
|
|
}.by(1)
|
|
end
|
|
end
|
|
|
|
context "when the alarm is for a the same target" do
|
|
let(:target) { "foo" }
|
|
|
|
it "does not duplicate the alarm" do
|
|
expect { problem_tracker.problem!(next_run_at: 24.hours.from_now) }.not_to change {
|
|
AdminNotice.problem.count
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when there are still blips to go" do
|
|
let(:blips) { 0 }
|
|
|
|
before { ProblemCheck::TwitterLogin.stubs(:max_blips).returns(1) }
|
|
|
|
it "does not sound the alarm" do
|
|
expect { problem_tracker.problem!(next_run_at: 24.hours.from_now) }.not_to change {
|
|
AdminNotice.problem.count
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#no_problem!" do
|
|
let(:next_run_at) { 24.hours.from_now.round(6) }
|
|
|
|
let(:problem_tracker) do
|
|
Fabricate(:problem_check_tracker, identifier: "twitter_login", **original_attributes)
|
|
end
|
|
|
|
let(:original_attributes) do
|
|
{
|
|
blips: 0,
|
|
last_problem_at: 1.week.ago,
|
|
last_success_at: Time.current,
|
|
last_run_at: 24.hours.ago,
|
|
next_run_at: nil,
|
|
}
|
|
end
|
|
|
|
let(:updated_attributes) { { blips: 0, next_run_at: } }
|
|
|
|
it do
|
|
freeze_time
|
|
|
|
expect { problem_tracker.no_problem!(next_run_at:) }.to change {
|
|
problem_tracker.attributes
|
|
}.to(hash_including(updated_attributes))
|
|
end
|
|
|
|
context "when there's an alarm sounding" do
|
|
before { problem_tracker.problem! }
|
|
|
|
it "silences the alarm" do
|
|
expect { problem_tracker.no_problem!(next_run_at: 24.hours.from_now) }.to change {
|
|
AdminNotice.problem.count
|
|
}.by(-1)
|
|
end
|
|
end
|
|
end
|
|
end
|