Repository: hyperjumptech/monika
Pull request #620 (2022-02-25 08:12:21)

chore: send recovery/incident after the first loop to Symon
This PR solves #578 

codecov[bot]: # [Codecov](https://codecov.io/gh/hyperjumptech/monika/pull/620?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech) Report
> Merging [#620](https://codecov.io/gh/hyperjumptech/monika/pull/620?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech) (f99de7f) into [main](https://codecov.io/gh/hyperjumptech/monika/commit/488c8a8c7516583d50ad5dedd282c991632ad93e?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech) (488c8a8) will **increase** coverage by `0.04%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/hyperjumptech/monika/pull/620/graphs/tree.svg?width=650&height=150&src=pr&token=O3WVT7DP6F&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech)](https://codecov.io/gh/hyperjumptech/monika/pull/620?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech)

```diff
@@            Coverage Diff             @@
##             main     #620      +/-   ##
==========================================
+ Coverage   55.36%   55.41%   +0.04%     
==========================================
  Files          62       62              
  Lines        1864     1866       +2     
  Branches      360      360              
==========================================
+ Hits         1032     1034       +2     
  Misses        693      693              
  Partials      139      139              
```


| [Impacted Files](https://codecov.io/gh/hyperjumptech/monika/pull/620?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech) | Coverage Δ | |
|---|---|---|
| [...c/components/notification/process-server-status.ts](https://codecov.io/gh/hyperjumptech/monika/pull/620/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech#diff-c3JjL2NvbXBvbmVudHMvbm90aWZpY2F0aW9uL3Byb2Nlc3Mtc2VydmVyLXN0YXR1cy50cw==) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/hyperjumptech/monika/pull/620?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/hyperjumptech/monika/pull/620?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech). Last update [488c8a8...f99de7f](https://codecov.io/gh/hyperjumptech/monika/pull/620?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech).


Patch:
diff --git a/.tool-versions b/.tool-versions
new file mode 100644
index 00000000..33f48bc6
--- /dev/null
+++ b/.tool-versions
@@ -0,0 +1,1 @@
+nodejs 14.18.3
diff --git a/src/components/notification/process-server-status.ts b/src/components/notification/process-server-status.ts
index c9fbee2d..b3ecfef6 100644
--- a/src/components/notification/process-server-status.ts
+++ b/src/components/notification/process-server-status.ts
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
 /**********************************************************************************
  * MIT License                                                                    *
  *                                                                                *
@@ -33,6 +34,7 @@ export type ServerAlertStateContext = {
   recoveryThreshold: number
   consecutiveFailures: number
   consecutiveSuccesses: number
+  isFirstTimeSendEvent: boolean
 }
 
 type ShouldSendNotification = {
@@ -44,6 +46,7 @@ type ShouldSendNotification = {
 }
 
 type ShouldSendNotificationReturn = {
+  isFirstTime: boolean
   state: 'UP' | 'DOWN'
   shouldSendNotification: boolean
 }
@@ -60,6 +63,9 @@ export const serverAlertStateMachine = createMachine<ServerAlertStateContext>(
     states: {
       UP: {
         on: {
+          FIST_TIME_EVENT_SENT: {
+            actions: 'handleFirstTimeEventSent',
+          },
           FAILURE: [
             {
               target: 'DOWN',
@@ -77,6 +83,9 @@ export const serverAlertStateMachine = createMachine<ServerAlertStateContext>(
       },
       DOWN: {
         on: {
+          FIST_TIME_EVENT_SENT: {
+            actions: 'handleFirstTimeEventSent',
+          },
           FAILURE: {
             actions: 'handleFailure',
           },
@@ -96,6 +105,9 @@ export const serverAlertStateMachine = createMachine<ServerAlertStateContext>(
   },
   {
     actions: {
+      handleFirstTimeEventSent: assign({
+        isFirstTimeSendEvent: (_context) => false,
+      }),
       handleFailure: assign({
         consecutiveFailures: (context) => context.consecutiveFailures + 1,
         consecutiveSuccesses: (_context) => 0,
@@ -142,6 +154,7 @@ export const processThresholds = ({
         recoveryThreshold,
         consecutiveFailures: 0,
         consecutiveSuccesses: 0,
+        isFirstTimeSendEvent: true,
       })
 
       interpreters[alert.query] = interpret(stateMachine).start()
@@ -163,15 +176,20 @@ export const processThresholds = ({
 
     interpreter.send(isAlertTriggered ? 'FAILURE' : 'SUCCESS')
 
-    const state = interpreter.state
+    const stateValue = interpreter.state.value
+    const stateContext = interpreter.state.context
 
     results.push({
+      isFirstTime: stateContext.isFirstTimeSendEvent,
       alertQuery: alert.query,
-      state: state.value as 'UP' | 'DOWN',
+      state: stateValue as 'UP' | 'DOWN',
       shouldSendNotification:
-        (state.value === 'DOWN' && prevStateValue === 'UP') ||
-        (state.value === 'UP' && prevStateValue === 'DOWN'),
+        stateContext.isFirstTimeSendEvent ||
+        (stateValue === 'DOWN' && prevStateValue === 'UP') ||
+        (stateValue === 'UP' && prevStateValue === 'DOWN'),
     })
+
+    interpreter.send('FIST_TIME_EVENT_SENT')
   }
 
   return results
@@ -194,6 +212,7 @@ export function getNotificationState({
       recoveryThreshold,
       consecutiveFailures: 0,
       consecutiveSuccesses: 0,
+      isFirstTimeSendEvent: true,
     })
 
     interpreters[alertQuery] = interpret(stateMachine).start()
@@ -207,10 +226,15 @@ export function getNotificationState({
   interpreter?.send(isAlertTriggered ? 'FAILURE' : 'SUCCESS')
 
   const currentStateValue = interpreter?.state?.value
+  const stateContext = interpreter?.state?.context
+
+  interpreter.send('FIST_TIME_EVENT_SENT')
 
   return {
+    isFirstTime: stateContext.isFirstTimeSendEvent,
     state: currentStateValue as 'UP' | 'DOWN',
     shouldSendNotification:
+      stateContext.isFirstTimeSendEvent ||
       (currentStateValue === 'DOWN' &&
         (prevStateValue === 'UP' || !prevStateValue)) ||
       (currentStateValue === 'UP' && prevStateValue === 'DOWN'),
diff --git a/src/components/probe/index.ts b/src/components/probe/index.ts
index 573b9ce0..75224775 100644
--- a/src/components/probe/index.ts
+++ b/src/components/probe/index.ts
@@ -107,6 +107,7 @@ async function checkThresholdsAndSendAlert(
   }
 
   statuses
+    ?.filter((probeState) => !probeState.isFirstTime)
     ?.filter((probeState) => probeState.shouldSendNotification)
     ?.forEach((probeState, index) => {
       probeSendNotification({
@@ -283,7 +284,7 @@ async function processTCPRequestResult({
 }: ProcessTCPRequestResult) {
   const defaultAlertQuery = 'response.size < 1'
   const defaultMessage = 'Response size is 0, expecting more than 0'
-  const { state, shouldSendNotification } = getNotificationState({
+  const { state, isFirstTime, shouldSendNotification } = getNotificationState({
     id: tcpRequestID,
     alertQuery: defaultAlertQuery,
     incidentThreshold,
@@ -291,7 +292,7 @@ async function processTCPRequestResult({
     isAlertTriggered,
   })
 
-  if (shouldSendNotification) {
+  if (shouldSendNotification && !isFirstTime) {
     // TODO: Remove validation below
     // validation is used because it is needed to send alert
     const validation: ValidatedResponse = {
diff --git a/src/interfaces/probe-status.ts b/src/interfaces/probe-status.ts
index d36b2836..89c4c414 100644
--- a/src/interfaces/probe-status.ts
+++ b/src/interfaces/probe-status.ts
@@ -23,6 +23,7 @@
  **********************************************************************************/
 
 export interface ServerAlertState {
+  isFirstTime: boolean
   alertQuery: string
   state: 'UP' | 'DOWN'
   shouldSendNotification: boolean


Test patch:
diff --git a/src/components/notification/__tests__/process-server-status.test.ts b/src/components/notification/__tests__/process-server-status.test.ts
index 9423f619..e290d11b 100644
--- a/src/components/notification/__tests__/process-server-status.test.ts
+++ b/src/components/notification/__tests__/process-server-status.test.ts
@@ -45,6 +45,7 @@ describe('serverAlertStateMachine', () => {
         recoveryThreshold: 5,
         consecutiveFailures: 0,
         consecutiveSuccesses: 0,
+        isFirstTimeSendEvent: true,
       })
     ).start()
   })
@@ -267,6 +268,14 @@ describe('Send notification based on threshold', () => {
   describe('not send notification', () => {
     it('the incident does not cross the threshold', () => {
       // act
+      getNotificationState({
+        id: 'random-id',
+        alertQuery: 'response.size < 0',
+        incidentThreshold: 3,
+        recoveryThreshold: 3,
+        isAlertTriggered: true,
+      })
+
       const { state, shouldSendNotification } = getNotificationState({
         id: 'random-id',
         alertQuery: 'response.size < 0',