Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions src/api/StatusApi.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { StatusApi, MutedAlert } from './StatusApi'
import { StatusRepository } from '../db/StatusRepository'

describe('StatusApi.getAgeAlerts', () => {
// The repository is not used by getAgeAlerts, so a bare object is enough.
const statusApi = new StatusApi({} as StatusRepository)

// A timestamp old enough to trigger a red alert
const staleDate = new Date(Date.now() - 100 * 60 * 60 * 1000)

const yellowAgeHours = 26
const redAgeHours = 50

it('creates a red alert for stale data when nothing is muted', () => {
const status = {
3701: { latestUpdates: { 'anchor/playsByGender': staleDate } },
}

const alerts = statusApi.getAgeAlerts(
status,
yellowAgeHours,
redAgeHours
)

expect(alerts.red).toHaveLength(1)
expect(alerts.red?.[0]).toMatchObject({
podcastId: 3701,
endpoint: 'anchor/playsByGender',
})
})

it('suppresses alerts for a muted podcast/endpoint combination', () => {
const status = {
3701: { latestUpdates: { 'anchor/playsByGender': staleDate } },
}
const muted: MutedAlert[] = [
{ podcastId: 3701, endpoint: 'anchor/playsByGender' },
]

const alerts = statusApi.getAgeAlerts(
status,
yellowAgeHours,
redAgeHours,
muted
)

expect(alerts.red).toBeUndefined()
expect(alerts.yellow).toBeUndefined()
})

it('only mutes the specified endpoint, not other endpoints of the podcast', () => {
const status = {
3701: {
latestUpdates: {
'anchor/playsByGender': staleDate,
'anchor/plays': staleDate,
},
},
}
const muted: MutedAlert[] = [
{ podcastId: 3701, endpoint: 'anchor/playsByGender' },
]

const alerts = statusApi.getAgeAlerts(
status,
yellowAgeHours,
redAgeHours,
muted
)

expect(alerts.red).toHaveLength(1)
expect(alerts.red?.[0]).toMatchObject({
podcastId: 3701,
endpoint: 'anchor/plays',
})
})

it('only mutes the specified podcast, not the same endpoint on other podcasts', () => {
const status = {
3701: { latestUpdates: { 'anchor/playsByGender': staleDate } },
42: { latestUpdates: { 'anchor/playsByGender': staleDate } },
}
const muted: MutedAlert[] = [
{ podcastId: 3701, endpoint: 'anchor/playsByGender' },
]

const alerts = statusApi.getAgeAlerts(
status,
yellowAgeHours,
redAgeHours,
muted
)

expect(alerts.red).toHaveLength(1)
expect(alerts.red?.[0]).toMatchObject({
podcastId: 42,
endpoint: 'anchor/playsByGender',
})
})
})
25 changes: 24 additions & 1 deletion src/api/StatusApi.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { StatusRepository } from '../db/StatusRepository'
import { StatusPayload } from '../types/api'

// An alert that is intentionally muted, identified by the podcast (account id)
// and the endpoint identifier in the `provider/endpoint` format used by
// `getStatus` (e.g. `anchor/playsByGender`).
export type MutedAlert = {
podcastId: number
endpoint: string
}

class StatusApi {
statusRepo: StatusRepository

Expand All @@ -15,14 +23,19 @@ class StatusApi {

// checks all dates and create yellow and red alerts if too old
// returns an object with the alerts if there are any
//
// `mutedAlerts` lists podcast/endpoint combinations that should never
// generate an alert, e.g. for podcasts that only report a given endpoint
// sporadically and would otherwise produce noisy false positives.
getAgeAlerts(
statusData: {
[podcastId: number]: {
latestUpdates: { [endpointName: string]: Date }
}
},
yellowAgeHours: number,
redAgeHours: number
redAgeHours: number,
mutedAlerts: MutedAlert[] = []
) {
type AlertData = {
podcastId: number
Expand All @@ -36,6 +49,16 @@ class StatusApi {
const podcastId = parseInt(podcastIdStr)
Object.entries(podcastData.latestUpdates).forEach(
([endpointName, endpointDate]) => {
// skip alerts that have been explicitly muted for this
// podcast/endpoint combination
const isMuted = mutedAlerts.some(
(muted) =>
muted.podcastId === podcastId &&
muted.endpoint === endpointName
)
if (isMuted) {
return
}
const ageHours = Math.round(
(new Date().getTime() -
new Date(endpointDate).getTime()) /
Expand Down
18 changes: 18 additions & 0 deletions src/api/mutedAlerts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { MutedAlert } from './StatusApi'

// Alerts that are intentionally muted in the `/status` age-alert check.
//
// Some podcasts only report certain endpoints sporadically. For those, stale
// data is expected and would otherwise produce noisy yellow/red alerts. Add an
// entry here to silence a single podcast/endpoint combination without affecting
// the rest of the podcast's monitoring (use the podcast-wide `monitored` flag
// in the `podcasts` table if you want to mute an entire podcast instead).
//
// `endpoint` uses the `provider/endpoint` identifier from `getStatus`,
// e.g. `anchor/playsByGender`.
export const MUTED_ALERTS: MutedAlert[] = [
// account_id 3701: the show is not very active and only reports
// anchor/playsByGender occasionally, so the age check would otherwise fire
// false-positive alerts. Mute it.
{ podcastId: 3701, endpoint: 'anchor/playsByGender' },
]
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { FeedbackRepository } from './db/FeedbackRepository'
import { FeedbackApi } from './api/FeedbackApi'
import { StatusRepository } from './db/StatusRepository'
import { StatusApi } from './api/StatusApi'
import { MUTED_ALERTS } from './api/mutedAlerts'
import crypto from 'crypto'
import { body, validationResult } from 'express-validator'
import { Config } from './config'
Expand Down Expand Up @@ -429,7 +430,8 @@ app.get('/status', async (req: Request, res: Response, next: NextFunction) => {
const alerts = statusApi.getAgeAlerts(
status,
yellowAgeHours,
redAgeHours
redAgeHours,
MUTED_ALERTS
)
// the key yellow or red is is only set if there is an alert
// this allows the client to check for the existence of the keys yellow and red
Expand Down
Loading