-
Notifications
You must be signed in to change notification settings - Fork 38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[MDS-5607] - Exception handling + Email Notification #2824
Changes from 16 commits
99eccff
b2c96e2
d475ee6
bc2bf67
c0e01a6
93ca286
10f8c82
9d6f72e
8318a31
baf99b4
f99a20c
4b9d080
88e1046
8c5ce1b
d2f779f
954c382
8bcefca
ef6778f
e0ae94a
7dc4026
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,7 @@ export const DEFAULT_ENVIRONMENT = { | |
keycloak_url: "https://test.loginproxy.gov.bc.ca/auth", | ||
flagsmithKey: "4Eu9eEMDmWVEHKDaKoeWY7", | ||
flagsmithUrl: "https://mds-flags-dev.apps.silver.devops.gov.bc.ca/api/v1/", | ||
errorNotifyRecipients: "[email protected]" | ||
}; | ||
|
||
export const ENVIRONMENT = { | ||
|
@@ -23,6 +24,7 @@ export const ENVIRONMENT = { | |
environment: "<ENV>", | ||
flagsmithKey: "<FLAGSMITH_KEY>", | ||
flagsmithUrl: "<FLAGSMITH_URL>", | ||
errorNotifyRecipients: "<ERROR_NOTIFY_RECIPIENTS>", | ||
_loaded: false, | ||
}; | ||
|
||
|
@@ -55,7 +57,8 @@ export function setupEnvironment( | |
matomoUrl, | ||
environment, | ||
flagsmithKey, | ||
flagsmithUrl | ||
flagsmithUrl, | ||
errorNotifyRecipients | ||
) { | ||
if (!apiUrl) { | ||
throw new Error("apiUrl Is Mandatory"); | ||
|
@@ -82,14 +85,17 @@ export function setupEnvironment( | |
if (!flagsmithUrl) { | ||
throw new Error("flagsmithUrl Is Mandatory"); | ||
} | ||
|
||
if (!errorNotifyRecipients) { | ||
throw new Error("errorNotifyRecipients is Mandatory") | ||
} | ||
ENVIRONMENT.apiUrl = apiUrl; | ||
ENVIRONMENT.docManUrl = docManUrl; | ||
ENVIRONMENT.filesystemProviderUrl = filesystemProviderUrl; | ||
ENVIRONMENT.matomoUrl = matomoUrl; | ||
ENVIRONMENT.environment = environment || "development"; | ||
ENVIRONMENT.flagsmithKey = flagsmithKey; | ||
ENVIRONMENT.flagsmithUrl = flagsmithUrl; | ||
ENVIRONMENT.errorNotifyRecipients = errorNotifyRecipients; | ||
|
||
ENVIRONMENT._loaded = true; | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,10 @@ | ||
import axios from "axios"; | ||
import { notification } from "antd"; | ||
import { notification, Button } from "antd"; | ||
import * as String from "@mds/common/constants/strings"; | ||
import React from 'react'; | ||
import * as API from "@mds/common/constants/API"; | ||
import { ENVIRONMENT } from "@mds/common"; | ||
import { createRequestHeader } from "./utils/RequestHeaders"; | ||
|
||
// https://stackoverflow.com/questions/39696007/axios-with-promise-prototype-finally-doesnt-work | ||
// eslint-disable-next-line @typescript-eslint/no-var-requires | ||
|
@@ -15,6 +19,61 @@ const formatErrorMessage = (errorMessage) => { | |
return errorMessage.replace("(psycopg2.", "(DatabaseError."); | ||
}; | ||
|
||
const decodeJWT = (token) => { | ||
var base64Url = token.split('.')[1]; | ||
var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); | ||
var jsonPayload = decodeURIComponent(window.atob(base64).split('').map(function(c) { | ||
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); | ||
}).join('')); | ||
|
||
return JSON.parse(jsonPayload); | ||
}; | ||
|
||
const notifymAdmin = (error) => { | ||
|
||
const business_message = error?.response?.data?.message; | ||
const detailed_error = error?.response?.data?.detailed_error; | ||
let date = new Date() | ||
const reported_date = `${date} ${date.getHours()}:${date.getMinutes()}`; | ||
const user = decodeJWT(createRequestHeader().headers.Authorization) | ||
const environment_name = ENVIRONMENT.environment.toString().toUpperCase(); | ||
|
||
const email_title = "[MDS_INCIDENT_REPORT] [" + environment_name + "] - " + reported_date + " - " + business_message; | ||
const email_body = `<p><b>Environment:</b> ${environment_name}</P> | ||
<p><b>Business Error:</b> ${business_message}</P> | ||
<p> | ||
<b>Reporter's Name:</b> ${user.given_name} ${user.family_name}</br> | ||
<b>Reporter's Email:</b> ${user.email}</br> | ||
<b>Reporter's IDIR:</b> ${user.idir_username}<br/> | ||
<b>Reported Date:</b> ${reported_date} | ||
</P> | ||
<p><b>Detailed error:</b> ${detailed_error}</P><br/> | ||
<p>To create a Jira ticket | ||
<a href="https://bcmines.atlassian.net/jira/software/c/projects/MDS/boards/34/backlog"> | ||
Click here</a> </P><br/>`; | ||
|
||
// TODO Recipients needs to be read from property file. | ||
const payload = { | ||
"title" : email_title, | ||
"body": email_body, | ||
"recipients" : ENVIRONMENT.errorNotifyRecipients | ||
}; | ||
|
||
console.log("Sending error details to ", ENVIRONMENT.errorNotifyRecipients) | ||
|
||
CustomAxios().post(ENVIRONMENT.apiUrl + API.COMMONS_EMAIL, payload, createRequestHeader()) | ||
.then((response) => { | ||
notification.success({ | ||
message: "Error details sent to Admin. Thank you.", | ||
duration: 5, | ||
}); | ||
return response; | ||
}) | ||
.catch((err) => { | ||
throw new Error(err); | ||
}) | ||
}; | ||
|
||
// @ts-ignore | ||
const CustomAxios = ({ errorToastMessage, suppressErrorNotification = false } = {}) => { | ||
const instance = axios.create(); | ||
|
@@ -34,9 +93,22 @@ const CustomAxios = ({ errorToastMessage, suppressErrorNotification = false } = | |
(errorToastMessage === "default" || errorToastMessage === undefined) && | ||
!suppressErrorNotification | ||
) { | ||
console.error('Detailed Error: ', error?.response?.data?.detailed_error) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missed a console log 😜 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks @matbusby-fw, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, I don't suppose it will hurt anything, but it might be of limited usefulness to us since we're not going to be seeing the user's console. |
||
const notificationKey = 'errorNotification'; | ||
notification.error({ | ||
key: notificationKey, | ||
message: formatErrorMessage(error?.response?.data?.message ?? String.ERROR), | ||
description: <p style={{ color: 'grey' }}>If you think this is a system error please help us to improve by informing the system Admin</p>, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we put this behind a feature flag in case we decide on doing any iterations on this before releasing? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a feature flag from FE. Thanks |
||
duration: 10, | ||
btn: ( | ||
<Button type="primary" size="small" | ||
onClick={() => { | ||
notifymAdmin(error); | ||
notification.close(notificationKey); | ||
}}> | ||
Tell Admin | ||
</Button> | ||
), | ||
}); | ||
} else if (errorToastMessage && !suppressErrorNotification) { | ||
notification.error({ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
from flask_restplus import Namespace | ||
|
||
from app.api.commons.resources.email_resource import EmailResource | ||
|
||
api = Namespace('commons', description='Common operations') | ||
|
||
api.add_resource(EmailResource, '/email') |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
from app.api.services.email_service import EmailService | ||
from flask_restplus import Resource | ||
from flask import request | ||
|
||
from app.api.utils.access_decorators import (requires_any_of, VIEW_ALL) | ||
from app.api.utils.resources_mixins import UserMixin | ||
from app.api.exception.mds_core_api_exceptions import MDSCoreAPIException | ||
|
||
class EmailResource(Resource, UserMixin): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would suggest that we maybe do not expose this as a generic way of sending emails accross the app. I've got some concerns with this in terms of potential abuse, as this would in essence allow anyone with access to send arbitrary emails on behalf of the mds team. The other concern I have would be that we allow plain html as input which would open up for html injection As an alternative we could make this a specific "Report Error" endpoint that takes in the required fields so we can do some basic validation on it and create an email template that is specific for the error reporting in the templates folder which would escape what's passed in. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks Simen, |
||
|
||
@requires_any_of([VIEW_ALL]) | ||
def post(self): | ||
try: | ||
data = request.get_json() | ||
# Extract title and body from the JSON data | ||
title = data.get('title', '') | ||
body = data.get('body', '') | ||
recipients = data.get("recipients") | ||
|
||
EmailService.send_email(title, recipients, body) | ||
|
||
except Exception as e: | ||
raise MDSCoreAPIException("Error in sending email", detailed_error = e) | ||
|
||
return True, 201 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
class MDSCoreAPIException(Exception): | ||
"""A base Exception class for MDS Core API""" | ||
|
||
description = ( | ||
"Exception occurred in MDS Core API" | ||
) | ||
|
||
def __init__(self, message="Oops! Something went wrong", **kwargs): | ||
super().__init__(message) | ||
self.code = int(kwargs.get("status_code", 500)) | ||
self.message = message | ||
self.detailed_error = kwargs.get("detailed_error", "") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import json | ||
from app.api.exception.mds_core_api_exceptions import MDSCoreAPIException | ||
|
||
class MineException(MDSCoreAPIException): | ||
"""A Custom Exception for Errors in MINE Namespace""" | ||
|
||
description = ( | ||
"Generic exeption for errors occurred in the Mine module" | ||
) | ||
def __init__(self, message = "Oops! Something went wrong", **kwargs): | ||
super().__init__(message, **kwargs) | ||
self.code = int(kwargs.get("status_code", 500)) | ||
|
||
class ExplosivesPermitDocumentExeption(MineException): | ||
matbusby-fw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"""Exception for Explosives Permit Document Related Exception""" | ||
|
||
description = ( | ||
"Exception For Explosives Permit Document Related Errors" | ||
) | ||
def __init__(self, message = "An error occurred while processing Explosives Permit Document", **kwargs): | ||
super().__init__(message, **kwargs) | ||
self.code = int(kwargs.get("status_code", 500)) | ||
|
||
class ExplosivesPermitExeption(MineException): | ||
"""Exception for Explosive Permit related errors""" | ||
|
||
description = ( | ||
"Exception for errors in Explosive Permit" | ||
) | ||
def __init__(self, message = "Error in Explosive Permit", **kwargs): | ||
super().__init__(message, **kwargs) | ||
self.code = int(kwargs.get("status_code", 500)) | ||
|
||
class ExplosivePermitNumberAlreadyExistExeption(MineException): | ||
"""Exception for already existing permit number""" | ||
|
||
description = ( | ||
"Exception for already existing permit number" | ||
) | ||
def __init__(self, message = "A record already exists with the provided 'Explosives Permit Number'", **kwargs): | ||
super().__init__(message, **kwargs) | ||
self.code = int(kwargs.get("status_code", 422)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the change suggested above to make the email endpoint specific to error reporting, I would suggest trying to get these details (user email / username etc) from the users token on the endpoint level so you would not be able to send these on behalf of another user
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is addressed. Thanks.