feat: Capture unhandled errors by restructuring apps

This commit is contained in:
Faruk AYDIN
2022-10-21 19:03:24 +02:00
parent 525472d3e0
commit 9a743fb4a8
16 changed files with 92 additions and 76 deletions

View File

@@ -1,13 +1,16 @@
import { IGlobalVariable, IJSONObject } from "@automatisch/types"; import { IGlobalVariable, IJSONObject } from '@automatisch/types';
import type { AxiosResponse } from 'axios'; import type { AxiosResponse } from 'axios';
import parseLinkHeader from '../../../helpers/parse-header-link'; import parseLinkHeader from '../../../helpers/parse-header-link';
type TResponse = { type TResponse = {
data: IJSONObject[], data: IJSONObject[];
error?: IJSONObject, error?: IJSONObject;
} };
export default async function paginateAll($: IGlobalVariable, request: Promise<AxiosResponse>) { export default async function paginateAll(
$: IGlobalVariable,
request: Promise<AxiosResponse>
) {
const response = await request; const response = await request;
const aggregatedResponse: TResponse = { const aggregatedResponse: TResponse = {
data: [...response.data], data: [...response.data],
@@ -21,8 +24,8 @@ export default async function paginateAll($: IGlobalVariable, request: Promise<A
url: links.next.uri, url: links.next.uri,
}); });
if (nextPageResponse.integrationError) { if (nextPageResponse.httpError) {
aggregatedResponse.error = nextPageResponse.integrationError; aggregatedResponse.error = nextPageResponse.httpError;
links = null; links = null;
} else { } else {

View File

@@ -1,12 +1,11 @@
import { import { IGlobalVariable, ITriggerOutput } from '@automatisch/types';
IGlobalVariable,
ITriggerOutput,
} from '@automatisch/types';
import getRepoOwnerAndRepo from '../../common/get-repo-owner-and-repo'; import getRepoOwnerAndRepo from '../../common/get-repo-owner-and-repo';
import parseLinkHeader from '../../../../helpers/parse-header-link'; import parseLinkHeader from '../../../../helpers/parse-header-link';
function getPathname($: IGlobalVariable) { function getPathname($: IGlobalVariable) {
const { repoOwner, repo } = getRepoOwnerAndRepo($.step.parameters.repo as string); const { repoOwner, repo } = getRepoOwnerAndRepo(
$.step.parameters.repo as string
);
if (repoOwner && repo) { if (repoOwner && repo) {
return `/repos/${repoOwner}/${repo}/issues`; return `/repos/${repoOwner}/${repo}/issues`;
@@ -35,8 +34,8 @@ const newIssues = async ($: IGlobalVariable) => {
const response = await $.http.get(pathname, { params }); const response = await $.http.get(pathname, { params });
links = parseLinkHeader(response.headers.link); links = parseLinkHeader(response.headers.link);
if (response.integrationError) { if (response.httpError) {
issues.error = response.integrationError; issues.error = response.httpError;
return issues; return issues;
} }
@@ -44,7 +43,8 @@ const newIssues = async ($: IGlobalVariable) => {
for (const issue of response.data) { for (const issue of response.data) {
const issueId = issue.id; const issueId = issue.id;
if (issueId <= Number($.flow.lastInternalId) && !$.execution.testRun) return issues; if (issueId <= Number($.flow.lastInternalId) && !$.execution.testRun)
return issues;
const dataItem = { const dataItem = {
raw: issue, raw: issue,

View File

@@ -10,10 +10,12 @@ import parseLinkHeader from '../../../../helpers/parse-header-link';
type TResponseDataItem = { type TResponseDataItem = {
starred_at: string; starred_at: string;
user: IJSONObject; user: IJSONObject;
} };
const fetchStargazers = async ($: IGlobalVariable) => { const fetchStargazers = async ($: IGlobalVariable) => {
const { repoOwner, repo } = getRepoOwnerAndRepo($.step.parameters.repo as string); const { repoOwner, repo } = getRepoOwnerAndRepo(
$.step.parameters.repo as string
);
const firstPagePathname = `/repos/${repoOwner}/${repo}/stargazers`; const firstPagePathname = `/repos/${repoOwner}/${repo}/stargazers`;
const requestConfig = { const requestConfig = {
params: { params: {
@@ -22,10 +24,13 @@ const fetchStargazers = async ($: IGlobalVariable) => {
headers: { headers: {
// needed to get `starred_at` time // needed to get `starred_at` time
Accept: 'application/vnd.github.star+json', Accept: 'application/vnd.github.star+json',
} },
} };
const firstPageResponse = await $.http.get<TResponseDataItem[]>(firstPagePathname, requestConfig); const firstPageResponse = await $.http.get<TResponseDataItem[]>(
firstPagePathname,
requestConfig
);
const firstPageLinks = parseLinkHeader(firstPageResponse.headers.link); const firstPageLinks = parseLinkHeader(firstPageResponse.headers.link);
// in case there is only single page to fetch // in case there is only single page to fetch
@@ -36,12 +41,15 @@ const fetchStargazers = async ($: IGlobalVariable) => {
}; };
do { do {
const response = await $.http.get<TResponseDataItem[]>(pathname, requestConfig); const response = await $.http.get<TResponseDataItem[]>(
pathname,
requestConfig
);
const links = parseLinkHeader(response.headers.link); const links = parseLinkHeader(response.headers.link);
pathname = links.prev?.uri; pathname = links.prev?.uri;
if (response.integrationError) { if (response.httpError) {
stargazers.error = response.integrationError; stargazers.error = response.httpError;
return stargazers; return stargazers;
} }
@@ -50,7 +58,8 @@ const fetchStargazers = async ($: IGlobalVariable) => {
const { starred_at, user } = starEntry; const { starred_at, user } = starEntry;
const timestamp = DateTime.fromISO(starred_at).toMillis(); const timestamp = DateTime.fromISO(starred_at).toMillis();
if (timestamp <= Number($.flow.lastInternalId) && !$.execution.testRun) return stargazers; if (timestamp <= Number($.flow.lastInternalId) && !$.execution.testRun)
return stargazers;
const dataItem = { const dataItem = {
raw: user, raw: user,
@@ -65,13 +74,15 @@ const fetchStargazers = async ($: IGlobalVariable) => {
} while (pathname && !$.execution.testRun); } while (pathname && !$.execution.testRun);
return stargazers; return stargazers;
} };
const newStargazers = async ($: IGlobalVariable) => { const newStargazers = async ($: IGlobalVariable) => {
const stargazers = await fetchStargazers($); const stargazers = await fetchStargazers($);
stargazers.data.sort((stargazerA, stargazerB) => { stargazers.data.sort((stargazerA, stargazerB) => {
return Number(stargazerA.meta.internalId) - Number(stargazerB.meta.internalId); return (
Number(stargazerA.meta.internalId) - Number(stargazerB.meta.internalId)
);
}); });
return stargazers; return stargazers;

View File

@@ -25,7 +25,7 @@ const findMessage = async ($: IGlobalVariable, options: FindMessageOptions) => {
data: { data: {
raw: data?.messages.matches[0], raw: data?.messages.matches[0],
}, },
error: response?.integrationError || (!data.ok && data), error: response?.httpError || (!data.ok && data),
}; };
return message; return message;

View File

@@ -16,7 +16,7 @@ const postMessage = async (
data: { data: {
raw: response?.data?.message, raw: response?.data?.message,
}, },
error: response?.integrationError, error: response?.httpError,
}; };
if (response.data.ok === false) { if (response.data.ok === false) {

View File

@@ -15,8 +15,8 @@ export default {
const response = await $.http.get('/conversations.list'); const response = await $.http.get('/conversations.list');
if (response.integrationError) { if (response.httpError) {
channels.error = response.integrationError; channels.error = response.httpError;
return channels; return channels;
} }

View File

@@ -40,7 +40,7 @@ export default defineAction({
data: { data: {
raw: response.data, raw: response.data,
}, },
error: response?.integrationError, error: response?.httpError,
}; };
return tweet; return tweet;

View File

@@ -33,8 +33,8 @@ const getUserFollowers = async (
response = await $.http.get(requestPath); response = await $.http.get(requestPath);
if (response.integrationError) { if (response.httpError) {
followers.error = response.integrationError; followers.error = response.httpError;
return followers; return followers;
} }

View File

@@ -36,8 +36,8 @@ const fetchTweets = async ($: IGlobalVariable, username: string) => {
response = await $.http.get(requestPath); response = await $.http.get(requestPath);
if (response.integrationError) { if (response.httpError) {
tweets.error = response.integrationError; tweets.error = response.httpError;
return tweets; return tweets;
} }

View File

@@ -33,4 +33,10 @@ export default defineTrigger({
async run($) { async run($) {
return await searchTweets($); return await searchTweets($);
}, },
sort($) {
$.output.data.sort((tweet, nextTweet) => {
return Number(tweet.meta.internalId) - Number(nextTweet.meta.internalId);
});
},
}); });

View File

@@ -1,20 +1,12 @@
import { import { IGlobalVariable, IJSONObject } from '@automatisch/types';
IGlobalVariable,
IJSONObject,
ITriggerOutput,
} from '@automatisch/types';
import qs from 'qs'; import qs from 'qs';
import { omitBy, isEmpty } from 'lodash'; import { omitBy, isEmpty } from 'lodash';
const fetchTweets = async ($: IGlobalVariable) => { const searchTweets = async ($: IGlobalVariable) => {
const searchTerm = $.step.parameters.searchTerm as string; const searchTerm = $.step.parameters.searchTerm as string;
let response; let response;
const tweets: ITriggerOutput = {
data: [],
};
do { do {
const params: IJSONObject = { const params: IJSONObject = {
query: searchTerm, query: searchTerm,
@@ -30,14 +22,8 @@ const fetchTweets = async ($: IGlobalVariable) => {
response = await $.http.get(requestPath); response = await $.http.get(requestPath);
if (response.integrationError) {
tweets.error = response.integrationError;
return tweets;
}
if (response.data.errors) { if (response.data.errors) {
tweets.error = response.data.errors; throw new Error(JSON.stringify(response.data.errors));
return tweets;
} }
if (response.data.meta.result_count > 0) { if (response.data.meta.result_count > 0) {
@@ -49,22 +35,10 @@ const fetchTweets = async ($: IGlobalVariable) => {
}, },
}; };
tweets.data.push(dataItem); $.output.data.push(dataItem);
}); });
} }
} while (response.data.meta.next_token && !$.execution.testRun); } while (response.data.meta.next_token && !$.execution.testRun);
return tweets;
};
const searchTweets = async ($: IGlobalVariable) => {
const tweets = await fetchTweets($);
tweets.data.sort((tweet, nextTweet) => {
return Number(tweet.meta.internalId) - Number(nextTweet.meta.internalId);
});
return tweets;
}; };
export default searchTweets; export default searchTweets;

View File

@@ -59,6 +59,10 @@ const globalVariable = async (
id: execution?.id, id: execution?.id,
testRun, testRun,
}, },
output: {
data: [],
error: null,
},
}; };
$.http = createHttpClient({ $.http = createHttpClient({

View File

@@ -39,8 +39,8 @@ export default function createHttpClient({
instance.interceptors.response.use( instance.interceptors.response.use(
(response) => response, (response) => response,
(error) => { (error) => {
error.response.integrationError = error.response.data; error.response.httpError = error.response.data;
return error.response; throw error;
} }
); );

View File

@@ -10,12 +10,7 @@ class App {
// Temporaryly restrict the apps we expose until // Temporaryly restrict the apps we expose until
// their actions/triggers are implemented! // their actions/triggers are implemented!
static temporaryList = [ static temporaryList = ['github', 'scheduler', 'slack', 'twitter'];
'github',
'scheduler',
'slack',
'twitter',
];
static async findAll(name?: string, stripFuncs = true): Promise<IApp[]> { static async findAll(name?: string, stripFuncs = true): Promise<IApp[]> {
if (!name) if (!name)

View File

@@ -20,5 +20,23 @@ export const processFlow = async (options: ProcessFlowOptions) => {
testRun: options.testRun, testRun: options.testRun,
}); });
return await triggerCommand.run($); try {
await triggerCommand.run($);
} catch (error) {
if (error?.response?.httpError) {
$.output.error = error.response.httpError;
} else {
try {
$.output.error = JSON.parse(error.message);
} catch {
$.output.error = { error: error.message };
}
}
}
if (triggerCommand?.sort) {
triggerCommand.sort($);
}
return $.output;
}; };

View File

@@ -213,7 +213,8 @@ export interface ITrigger {
dedupeStrategy?: 'greatest' | 'unique' | 'last'; dedupeStrategy?: 'greatest' | 'unique' | 'last';
substeps: ISubstep[]; substeps: ISubstep[];
getInterval?(parameters: IGlobalVariable['step']['parameters']): string; getInterval?(parameters: IGlobalVariable['step']['parameters']): string;
run($: IGlobalVariable): Promise<ITriggerOutput>; run($: IGlobalVariable): Promise<void | ITriggerOutput>;
sort?($: IGlobalVariable): void | ITriggerOutput;
} }
export interface IActionOutput { export interface IActionOutput {
@@ -279,11 +280,15 @@ export type IGlobalVariable = {
id: string; id: string;
testRun: boolean; testRun: boolean;
}; };
output: {
data: ITriggerDataItem[];
error?: IJSONObject;
}
process?: (triggerDataItem: ITriggerDataItem) => Promise<void>; process?: (triggerDataItem: ITriggerDataItem) => Promise<void>;
}; };
declare module 'axios' { declare module 'axios' {
interface AxiosResponse { interface AxiosResponse {
integrationError?: IJSONObject; httpError?: IJSONObject;
} }
} }