Skip to content

Commit

Permalink
Implement email stack trace (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
JorWo authored Mar 8, 2024
1 parent 85da2d5 commit 3ff8729
Show file tree
Hide file tree
Showing 8 changed files with 298 additions and 83 deletions.
2 changes: 1 addition & 1 deletion ui/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"eslint-plugin-tsdoc"
],
"rules": {
"@stylistic/max-len": ["error", { "code": 120 }],
"@stylistic/max-len": ["warn", { "code": 120 }],
"@stylistic/indent": ["error", 4],
"@stylistic/eol-last": ["error", "always"],
"@stylistic/quotes": ["error", "single", { "allowTemplateLiterals": true }],
Expand Down
3 changes: 1 addition & 2 deletions ui/src/app/about/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,7 @@ const About = () => (
<h3 className="text-text-color text-lg pb-1">How do I request a new grouping?</h3>
<p className="pb-7">
<a className="text-link-color hover:underline hover:text-link-hover-color"
href="https://uhawaii.atlassian.net/wiki/spaces/UHIAM/pages/13402308/
UH+Groupings+Request+Form"
href="https://uhawaii.atlassian.net/wiki/spaces/UHIAM/pages/13402308/UH+Groupings+Request+Form"
aria-label="A request form is available">A request form is available</a>.
</p>

Expand Down
2 changes: 1 addition & 1 deletion ui/src/components/layout/navbar/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const Navbar = async () => {
const currentUser = await getCurrentUser();

return (
<nav className="border-b-[1px] pointer-events-auto">
<nav className="bg-white border-b-[1px] pointer-events-auto sticky top-0">
<div className="container py-2">
<div className="flex justify-between">
<Link href="/" className="lg:inline hidden">
Expand Down
22 changes: 21 additions & 1 deletion ui/src/services/EmailService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,28 @@ export type EmailResult = {
text: string
}

/**
* Sends feedback to Groupings API to send email.
*
* @param feedback - the feedback
*
* @returns The EmailResult
*/
export const sendFeedback = async (feedback: Feedback): Promise<EmailResult & ApiError> => {
const currentUser = await getCurrentUser();
const endpoint = `${baseUrl}/email/send/feedback`;
return postRequest(endpoint, currentUser.uid, feedback);
return postRequest<EmailResult>(endpoint, currentUser.uid, feedback);
}

/**
* Sends feedback to Groupings API to send stack trace email.
*
* @param stackTrace - the stack trace
*
* @returns The EmailResult
*/
export const sendStackTrace = async (stackTrace: string): Promise<EmailResult & ApiError> => {
const currentUser = await getCurrentUser();
const endpoint = `${baseUrl}/email/send/stack-trace`;
return postRequest<EmailResult>(endpoint, currentUser.uid, stackTrace, 'text/plain');
}
155 changes: 123 additions & 32 deletions ui/src/services/FetchService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,23 @@

import { ApiError } from '../groupings/GroupingsApiResults';
import { getCurrentUser } from '@/access/AuthenticationService';
import { sendStackTrace } from './EmailService';

const maxRetries = 3;
const baseUrl = process.env.NEXT_PUBLIC_API_2_1_BASE_URL as string;

enum HTTPMethod {
GET = 'GET',
POST = 'POST',
PUT = 'PUT',
DELETE = 'DELETE'
}

enum Status {
COMPLETED = 'COMPLETED',
IN_PROGRESS = 'IN_PROGRESS'
}

/**
* Sleep/wait for the specified milliseconds.
*
Expand All @@ -24,10 +37,10 @@ const delay = async (ms = 5000) => new Promise((res) => setTimeout(res, ms));
*/
const poll = async <T> (jobId: number): Promise<T & ApiError> => {
const currentUser = await getCurrentUser();
return fetch(`${baseUrl}/jobs/${jobId}`, { headers: { 'current_user': currentUser.uid } })
.then(res => res.json())
return await fetch(`${baseUrl}/jobs/${jobId}`, { headers: { 'current_user': currentUser.uid } })
.then(res => handleFetch(res, HTTPMethod.GET))
.then(async res => {
if (res.status === 'COMPLETED') {
if (res.status === Status.COMPLETED) {
return res.result;
}
await delay();
Expand All @@ -49,7 +62,7 @@ export const getRequest = async <T> (
currentUserKey: string = ''
): Promise<T & ApiError> =>
await fetch(endpoint, { headers: { 'current_user': currentUserKey } })
.then(res => res.json())
.then(res => handleFetch(res, HTTPMethod.GET))
.catch(err => err);

/**
Expand All @@ -65,12 +78,17 @@ export const postRequest = async <T> (
endpoint: string,
currentUserKey: string,
body?: object | string | string[],
contentType = 'application/json'
): Promise<T & ApiError> =>
await fetch(endpoint, {
method: 'POST',
headers: { 'current_user': currentUserKey },
body: JSON.stringify(body)})
.then(res => res.json())
method: HTTPMethod.POST,
headers: {
'current_user': currentUserKey,
'Content-Type': contentType
},
body: stringifyBody(body)
})
.then(res => handleFetch(res, HTTPMethod.POST))
.catch(err => err);

/**
Expand All @@ -85,10 +103,18 @@ export const postRequest = async <T> (
export const postRequestAsync = async <T> (
endpoint: string,
currentUserKey: string,
body: string | string[]
body?: object | string | string[],
contentType = 'application/json'
): Promise<T & ApiError> =>
await fetch(endpoint, { method: 'POST', headers: { 'current_user': currentUserKey }, body: JSON.stringify(body) })
.then(res => res.json())
await fetch(endpoint, {
method: HTTPMethod.POST,
headers: {
'current_user': currentUserKey,
'Content-Type': contentType
},
body: stringifyBody(body)
})
.then(res => handleFetch(res, HTTPMethod.POST))
.then(res => poll<T>(res))
.catch(err => err);

Expand All @@ -104,16 +130,24 @@ export const postRequestAsync = async <T> (
export const postRequestRetry = async <T> (
endpoint: string,
currentUserKey: string,
body: string | string[],
body?: object | string | string[],
contentType = 'application/json',
retries: number = maxRetries
): Promise<T & ApiError> =>
await fetch(endpoint, { method: 'POST', headers: { 'current_user': currentUserKey }, body: JSON.stringify(body) })
await fetch(endpoint, {
method: HTTPMethod.POST,
headers: {
'current_user': currentUserKey,
'Content-Type': contentType
},
body: stringifyBody(body)
})
.then(async res => {
if (res.status === 500 && retries > 0) {
await delay(2000 * Math.log(maxRetries / retries));
return postRequestRetry(endpoint, currentUserKey, body, retries - 1);
return postRequestRetry(endpoint, currentUserKey, body, contentType, retries - 1);
}
return res.json();
return handleFetch(res, HTTPMethod.POST);
})
.catch(err => err);

Expand All @@ -130,13 +164,18 @@ export const postRequestRetry = async <T> (
export const putRequest = async <T> (
endpoint: string,
currentUserKey: string,
body?: object | string | string[]
body?: object | string | string[],
contentType = 'application/json'
): Promise<T & ApiError> =>
await fetch(endpoint, {
method: 'PUT',
headers: { 'current_user': currentUserKey },
body: JSON.stringify(body) })
.then(res => res.json())
method: HTTPMethod.PUT,
headers: {
'current_user': currentUserKey,
'Content-Type': contentType
},
body: stringifyBody(body)
})
.then(res => handleFetch(res, HTTPMethod.PUT))
.catch(err => err);

/**
Expand All @@ -151,10 +190,18 @@ export const putRequest = async <T> (
export const putRequestAsync = async <T> (
endpoint: string,
currentUserKey: string,
body: string | string[]
body?: object | string | string[],
contentType = 'application/json'
): Promise<T & ApiError> =>
await fetch(endpoint, { method: 'PUT', headers: { 'current_user': currentUserKey }, body: JSON.stringify(body) })
.then(res => res.json())
await fetch(endpoint, {
method: HTTPMethod.PUT,
headers: {
'current_user': currentUserKey,
'Content-Type': contentType
},
body: stringifyBody(body)
})
.then(res => handleFetch(res, HTTPMethod.PUT))
.then(res => poll<T>(res))
.catch(err => err);

Expand All @@ -170,11 +217,18 @@ export const putRequestAsync = async <T> (
export const deleteRequest = async <T> (
endpoint: string,
currentUserKey: string,
body?: object | string | string[]
body?: object | string | string[],
contentType = 'application/json'
): Promise<T & ApiError> =>
await fetch(endpoint, { method: 'DELETE', headers: { 'current_user': currentUserKey }, body: JSON.stringify(body) })
.then(res => res.json())
.then(res => (res))
await fetch(endpoint, {
method: HTTPMethod.DELETE,
headers: {
'current_user': currentUserKey,
'Content-Type': contentType
},
body: stringifyBody(body)
})
.then(res => handleFetch(res, HTTPMethod.DELETE))
.catch(err => err);

/**
Expand All @@ -189,12 +243,49 @@ export const deleteRequest = async <T> (
export const deleteRequestAsync = async <T> (
endpoint: string,
currentUserKey: string,
body?: object | string | string[]
body?: object | string | string[],
contentType = 'application/json'
): Promise<T & ApiError> =>
await fetch(endpoint, {
method: 'DELETE',
headers: { 'current_user': currentUserKey },
body: JSON.stringify(body) })
.then(res => res.json())
method: HTTPMethod.DELETE,
headers: {
'current_user': currentUserKey,
'Content-Type': contentType
},
body: stringifyBody(body)
})
.then(res => handleFetch(res, HTTPMethod.DELETE))
.then(res => poll<T>(res))
.catch(err => err);

/**
* Helper function for the .then clause of a fetch promise.
* Sends an email stack trace if an error is thrown.
*
* @param res - the response
* @param httpMethod - the HTTPMethod
*
* @returns The res.json()
*/
const handleFetch = (res: Response, httpMethod: HTTPMethod) => {
if (!res.ok) {
const error = Error(`${res.status} error from ${httpMethod} ${res.url}`);
sendStackTrace(error.stack as string);
}
return res.json();
}

/**
* Helper function to JSON.stringify the body of a request or keep as a string
* if the passed body is already a string.
*
* @param body - the body of a request
*
* @returns The stringified body
*/
const stringifyBody = (body: object | string | string[] | undefined) => {
if (typeof body === 'string') {
return body
}
return JSON.stringify(body)
};
5 changes: 2 additions & 3 deletions ui/tests/app/about/About.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {render, screen} from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import About from '@/app/about/page';

describe('About', () => {
Expand All @@ -14,8 +14,7 @@ describe('About', () => {

expect(screen.getByText('GENERAL INFO')).toBeInTheDocument();
expect(screen.getByRole('link', {name: 'A request form is available'}))
.toHaveAttribute('href', 'https://uhawaii.atlassian.net/wiki/spaces/UHIAM/pages/13402308/' +
' UH+Groupings+Request+Form');
.toHaveAttribute('href', 'https://uhawaii.atlassian.net/wiki/spaces/UHIAM/pages/13402308/UH+Groupings+Request+Form');
expect(screen.getByRole('link', {name: 'General information about groupings is available'}))
.toHaveAttribute('href', 'https://uhawaii.atlassian.net/wiki/spaces/UHIAM/pages/13403213/UH+Groupings');

Expand Down
27 changes: 23 additions & 4 deletions ui/tests/services/EmailService.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import User from '@/access/User';
import { Feedback, sendFeedback } from '@/services/EmailService';
import { Feedback, sendFeedback, sendStackTrace } from '@/services/EmailService';
import * as AuthenticationService from '@/access/AuthenticationService';

const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL as string;
Expand All @@ -10,7 +10,6 @@ jest.mock('@/access/AuthenticationService');
describe('EmailService', () => {

const currentUser = testUser;
const headers = { 'current_user': currentUser.uid };

beforeAll(() => {
jest.spyOn(AuthenticationService, 'getCurrentUser').mockResolvedValue(testUser);
Expand All @@ -29,10 +28,30 @@ describe('EmailService', () => {
await sendFeedback(feedback);
expect(fetch).toHaveBeenCalledWith(`${baseUrl}/email/send/feedback`, {
body: JSON.stringify(feedback),
headers,
headers: {
'current_user': currentUser.uid,
'Content-Type': 'application/json'
},
method: 'POST'
});
});

});

describe('sendStackTrace', () => {

const stackTrace = 'stackTrace';

it('should make a POST request at the correct endpoint', async () => {
await sendStackTrace(stackTrace);
expect(fetch).toHaveBeenCalledWith(`${baseUrl}/email/send/stack-trace`, {
body: stackTrace,
headers: {
'current_user': currentUser.uid,
'Content-Type': 'text/plain'
},
method: 'POST'
});
});
});

});
Loading

0 comments on commit 3ff8729

Please sign in to comment.