Skip to content

Commit

Permalink
Regression notification (#306)
Browse files Browse the repository at this point in the history
  • Loading branch information
ludeknovy authored Mar 8, 2024
1 parent a49c7a6 commit 4167903
Show file tree
Hide file tree
Showing 20 changed files with 375 additions and 35 deletions.
11 changes: 11 additions & 0 deletions migrations/1709293662942_notification-type.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
exports.up = (pgm) => {
pgm.createType("notification_type", ["report_detail", "degradation"] )
pgm.addColumn({ schema: "jtl", name: "notifications" }, {
notification_type: {
type: "notification_type",
"default": "report_detail",
notNull: true,
},
})
pgm.renameColumn({ schema: "jtl", name: "notifications" }, "type", "channel")
}
7 changes: 5 additions & 2 deletions src/server/controllers/item/shared/item-data-processing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
} from "../../../queries/items"
import { ReportStatus } from "../../../queries/items.model"
import { getScenarioSettings } from "../../../queries/scenario"
import { sendNotifications } from "../../../utils/notifications/send-notification"
import { sendDegradationNotifications, sendReportNotifications } from "../../../utils/notifications/send-notification"
import { scenarioThresholdsCalc } from "../utils/scenario-thresholds-calc"
import { extraIntervalMilliseconds } from "./extra-intervals-mapping"
import { AnalyticsEvent } from "../../../utils/analytics/anyltics-event"
Expand Down Expand Up @@ -139,11 +139,14 @@ export const itemDataProcessing = async ({ projectName, scenarioName, itemId })
const thresholdResult = scenarioThresholdsCalc(labelStats, baselineReport.stats, scenarioSettings)
if (thresholdResult) {
await db.none(saveThresholdsResult(projectName, scenarioName, itemId, thresholdResult))
if (!thresholdResult.passed) {
await sendDegradationNotifications(projectName, scenarioName, itemId)
}
}
}
}

await sendNotifications(projectName, scenarioName, itemId, overview)
await sendReportNotifications(projectName, scenarioName, itemId, overview)

await db.tx(async t => {
await t.none(saveItemStats(itemId, JSON.stringify(labelStats),
Expand Down
26 changes: 25 additions & 1 deletion src/server/controllers/item/utils/scenario-thresholds-calc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { divide } from "mathjs"
const PERC = 100

// eslint-disable-next-line max-len
export const scenarioThresholdsCalc = (labelStats: LabelStats[], baselineReportStats: LabelStats[], scenarioSettings) => {
export const scenarioThresholdsCalc = (labelStats: LabelStats[], baselineReportStats: LabelStats[], scenarioSettings): ThresholdValidation => {
const results = []
if (!scenarioSettings.errorRate || !scenarioSettings.percentile || !scenarioSettings.throughput) {
return undefined
Expand Down Expand Up @@ -71,3 +71,27 @@ export const scenarioThresholdsCalc = (labelStats: LabelStats[], baselineReportS
}
}

export interface ThresholdValidation {
passed: boolean
results: ThresholdResultExtended[]
thresholds: {
errorRate: number
throughput: number
percentile: number
}
}

interface ThresholdResultExtended {
passed: boolean
label: string
result: {
percentile: ThresholdResult
throughput: ThresholdValidation
errorRate: ThresholdResult
}
}

interface ThresholdResult {
passed: boolean
diffValue: boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { StatusCode } from "../../../utils/status-code"

export const createScenarioNotificationController = async (req: Request, res: Response) => {
const { projectName, scenarioName } = req.params
const { type, url, name } = req.body
await db.none(createScenarioNotification(projectName, scenarioName, type, url, name))
const { type, url, name, channel } = req.body
await db.none(createScenarioNotification(projectName, scenarioName, channel, url, name, type))
res.status(StatusCode.Created).send()
}
20 changes: 15 additions & 5 deletions src/server/queries/scenario.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,22 +151,32 @@ export const getProcessingItems = (projectName, scenarioName, environment) => {

export const scenarioNotifications = (projectName, scenarioName) => {
return {
text: `SELECT notif.id, url, type, notif.name FROM jtl.notifications as notif
text: `SELECT notif.id, url, channel, notification_type as "type", notif.name FROM jtl.notifications as notif
LEFT JOIN jtl.scenario as s ON s.id = notif.scenario_id
LEFT JOIN jtl.projects as p ON p.id = s.project_id
WHERE s.name = $2 AND p.project_name = $1`,
values: [projectName, scenarioName],
}
}

export const createScenarioNotification = (projectName, scenarioName, type, url, name) => {
export const scenarioNotificationsByType = (projectName, scenarioName, type) => {
return {
text: `INSERT INTO jtl.notifications(scenario_id, type, url, name) VALUES((
text: `SELECT notif.id, url, channel, notif.name FROM jtl.notifications as notif
LEFT JOIN jtl.scenario as s ON s.id = notif.scenario_id
LEFT JOIN jtl.projects as p ON p.id = s.project_id
WHERE s.name = $2 AND p.project_name = $1 AND notification_type = $3`,
values: [projectName, scenarioName, type],
}
}

export const createScenarioNotification = (projectName, scenarioName, channel, url, name, type) => {
return {
text: `INSERT INTO jtl.notifications(scenario_id, channel, url, name, notification_type) VALUES((
SELECT s.id FROM jtl.scenario as s
LEFT JOIN jtl.projects as p ON p.id = s.project_id
WHERE s.name = $2 AND p.project_name = $1
), $3, $4, $5)`,
values: [projectName, scenarioName, type, url, name],
), $3, $4, $5, $6)`,
values: [projectName, scenarioName, channel, url, name, type],
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/server/schema-validator/scenario-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ export const querySchema = {

export const scenarioNotificationBodySchema = {
url: Joi.string().max(URL_MAX_LENGTH).required(),
type: Joi.string().valid(["ms-teams", "gchat", "slack"]).required(),
channel: Joi.string().valid(["ms-teams", "gchat", "slack"]).required(),
name: Joi.string().min(1).max(MAX_NUMBER).required(),
type: Joi.string().valid("report_detail", "degradation").required(),
}


Expand Down
22 changes: 11 additions & 11 deletions src/server/utils/notifications/send-notification.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { db } from "../../../db/db"
import { sendNotifications } from "./send-notification"
import { sendReportNotifications } from "./send-notification"
const linkUrl = require("./link-url")
import axios from "axios"
const scenarioNotifications = require("../../queries/scenario")
const msTeamsTemplate = require("./templates/ms-teams-template")
const msTeamsTemplate = require("./templates/report/ms-teams-template")


jest.mock("axios")
jest.mock("./templates/ms-teams-template")
jest.mock("./templates/report/ms-teams-template")
jest.mock("../../../db/db")

const OVERVIEW = {
Expand All @@ -29,34 +29,34 @@ const OVERVIEW = {
describe("sendNotification", () => {
it("should call linkUrl", async () => {
const spy = jest.spyOn(linkUrl, "linkUrl")
await sendNotifications("test", "test", "id", OVERVIEW)
await sendReportNotifications("test", "test", "id", OVERVIEW)
expect(spy).toHaveBeenCalledTimes(1)
})
it("should trigger `scenarioNotifications` query", async () => {
const spy = jest.spyOn(scenarioNotifications, "scenarioNotifications")
await sendNotifications("test", "test", "id", OVERVIEW)
const spy = jest.spyOn(scenarioNotifications, "scenarioNotificationsByType")
await sendReportNotifications("test", "test", "id", OVERVIEW)
expect(spy).toHaveBeenCalledTimes(1)
})
it("should not send any request if no notifications found in db", async () => {
db.manyOrNone = jest.fn().mockImplementation(() => Promise.resolve([]))
await sendNotifications("test", "test", "id", OVERVIEW)
await sendReportNotifications("test", "test", "id", OVERVIEW)
expect(axios).not.toHaveBeenCalled()
})
it("should try to send notification request when found in db", async () => {
const spy = jest.spyOn(msTeamsTemplate, "msTeamsTemplate")
db.manyOrNone = jest.fn().mockImplementation(() =>
Promise.resolve([{ url: "test", name: "test-name", type: "ms-teams" }]))
Promise.resolve([{ url: "test", name: "test-name", channel: "ms-teams" }]))
const post = axios.post = jest.fn().mockImplementation(() => Promise.resolve({}))
await sendNotifications("test", "test", "id", OVERVIEW)
await sendReportNotifications("test", "test", "id", OVERVIEW)
expect(spy).toHaveBeenCalledTimes(1)
expect(post).toHaveBeenCalledTimes(1)
})
it("should not throw an error when request failed", () => {
db.manyOrNone = jest.fn().mockImplementation(() =>
Promise.resolve([{ url: "test", name: "test-name", type: "ms-teams" }]))
Promise.resolve([{ url: "test", name: "test-name", channel: "ms-teams" }]))
axios.post = jest.fn().mockImplementation(() => Promise.reject(new Error("failed")))
expect(async () => {
await sendNotifications("test", "test", "id", OVERVIEW)
await sendReportNotifications("test", "test", "id", OVERVIEW)
}).not.toThrow()
})
})
61 changes: 51 additions & 10 deletions src/server/utils/notifications/send-notification.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
import { db } from "../../../db/db"
import { scenarioNotifications } from "../../queries/scenario"
import { scenarioNotificationsByType } from "../../queries/scenario"
import axios from "axios"
import { msTeamsTemplate } from "./templates/ms-teams-template"
import { msTeamsTemplate } from "./templates/report/ms-teams-template"
import { logger } from "../../../logger"
import { linkUrl } from "./link-url"
import { Overview } from "../../data-stats/prepare-data"
import { gchatTemplate } from "./templates/gchat-template"
import { slackTemplate } from "./templates/slack-template"
import { gchatTemplate } from "./templates/report/gchat-template"
import { slackTemplate } from "./templates/report/slack-template"
import { msTeamsDegradationTemplate } from "./templates/degradation/ms-teams-degradation-template"
import { gchatDegradationTemplate } from "./templates/degradation/gchat-degradation-template"
import { slackDegradationTemplate } from "./templates/degradation/slack-degradation-template"

export const sendNotifications = async (projectName, scenarioName, id, overview: Overview) => {
export const sendReportNotifications = async (projectName, scenarioName, id, overview: Overview) => {
try {
const notifications: Notification[] = await db.manyOrNone(scenarioNotifications(projectName, scenarioName))
const notifications: Notification[] = await db.manyOrNone(
scenarioNotificationsByType(projectName, scenarioName, NotificationType.ReportDetail))
const url = linkUrl(projectName, scenarioName, id)
notifications.map(async (notif) => {
const messageTemplate = NotificationTemplate.get(notif.type)
const messageTemplate = NotificationReportTemplate.get(notif.channel)
try {
logger.info(`sending notification to: ${notif.type}`)
logger.info(`sending notification to: ${notif.channel}`)
const payload = messageTemplate(scenarioName, url, overview)
await axios.post(notif.url, payload, {
headers: {
Expand All @@ -31,16 +35,53 @@ export const sendNotifications = async (projectName, scenarioName, id, overview:
}
}

export const sendDegradationNotifications = async (projectName, scenarioName, id) => {
try {
const notifications: Notification[] = await db.manyOrNone(
scenarioNotificationsByType(projectName, scenarioName, NotificationType.Degradation))
const url = linkUrl(projectName, scenarioName, id)
notifications.map(async (notif) => {
const messageTemplate = NotificationDegradationTemplate.get(notif.channel)
try {
logger.info(`sending degradation notification to: ${notif.channel}`)
const payload = messageTemplate(scenarioName, url)
await axios.post(notif.url, payload, {
headers: {
"content-type": "application/json",
},
})
} catch(error) {
logger.error(`error while sending notification: ${error}`)
}
})
} catch(error) {
logger.error(`Error notification processing: ${error}`)
}

}

enum NotificationType {
ReportDetail = "report_detail",
Degradation = "degradation",
}


interface Notification {
id: string
url: string
type: string
channel: string
}

const NotificationTemplate = new Map<string, any>([
const NotificationReportTemplate = new Map<string, any>([
["ms-teams", msTeamsTemplate],
["gchat", gchatTemplate],
["slack", slackTemplate],
])

const NotificationDegradationTemplate =
new Map<string, any>([
["ms-teams", msTeamsDegradationTemplate],
["gchat", gchatDegradationTemplate],
["slack", slackDegradationTemplate],
])

Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { gchatDegradationTemplate } from "./gchat-degradation-template"

describe("GChat Degradation template", () => {
it("should return correct card payload when url provided", () => {
const template = gchatDegradationTemplate("scenarioName", "http://localhost")
expect(template).toEqual( {
cards: [
{
header: {
imageUrl: "",
subtitle: "Performance Degradation Detected for scenario: scenarioName",
title: "JTL Reporter",
},
sections: [
{
widgets: [
{
buttons: [
{
textButton: {
onClick: {
openLink: {
url: "http://localhost",
},
},
text: "OPEN RESULTS",
},
},
],
},
],
},
],
},
],
}
)
})
it("should return card payload when no url provided", () => {
const template = gchatDegradationTemplate("scenarioName", undefined)
expect(template).toEqual({
cards: [
{
header: {
imageUrl: "",
subtitle: "Performance Degradation Detected for scenario: scenarioName",
title: "JTL Reporter",
},
sections: [],
},
],
}
)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@

export const gchatDegradationTemplate = (scenarioName: string, url) => {
const cardPayload = {
cards: [{
header: {
title: "JTL Reporter",
subtitle: `Performance Degradation Detected for scenario: ${scenarioName}`,
imageUrl: "",
},
sections: [],
}],
}

if (url) {
(cardPayload.cards[0].sections as any).push({
widgets: [
{
buttons: [
{
textButton: {
text: "OPEN RESULTS",
onClick: {
openLink: {
url,
},
},
},
},
],
},
],
})
}
return cardPayload
}
Loading

0 comments on commit 4167903

Please sign in to comment.