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 parseLinkHeader from '../../../helpers/parse-header-link';
type TResponse = {
data: IJSONObject[],
error?: IJSONObject,
}
data: 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 aggregatedResponse: TResponse = {
data: [...response.data],
@@ -21,8 +24,8 @@ export default async function paginateAll($: IGlobalVariable, request: Promise<A
url: links.next.uri,
});
if (nextPageResponse.integrationError) {
aggregatedResponse.error = nextPageResponse.integrationError;
if (nextPageResponse.httpError) {
aggregatedResponse.error = nextPageResponse.httpError;
links = null;
} else {

View File

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

View File

@@ -10,10 +10,12 @@ import parseLinkHeader from '../../../../helpers/parse-header-link';
type TResponseDataItem = {
starred_at: string;
user: IJSONObject;
}
};
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 requestConfig = {
params: {
@@ -22,10 +24,13 @@ const fetchStargazers = async ($: IGlobalVariable) => {
headers: {
// needed to get `starred_at` time
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);
// in case there is only single page to fetch
@@ -36,12 +41,15 @@ const fetchStargazers = async ($: IGlobalVariable) => {
};
do {
const response = await $.http.get<TResponseDataItem[]>(pathname, requestConfig);
const response = await $.http.get<TResponseDataItem[]>(
pathname,
requestConfig
);
const links = parseLinkHeader(response.headers.link);
pathname = links.prev?.uri;
if (response.integrationError) {
stargazers.error = response.integrationError;
if (response.httpError) {
stargazers.error = response.httpError;
return stargazers;
}
@@ -50,7 +58,8 @@ const fetchStargazers = async ($: IGlobalVariable) => {
const { starred_at, user } = starEntry;
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 = {
raw: user,
@@ -65,13 +74,15 @@ const fetchStargazers = async ($: IGlobalVariable) => {
} while (pathname && !$.execution.testRun);
return stargazers;
}
};
const newStargazers = async ($: IGlobalVariable) => {
const stargazers = await fetchStargazers($);
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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -33,4 +33,10 @@ export default defineTrigger({
async run($) {
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 {
IGlobalVariable,
IJSONObject,
ITriggerOutput,
} from '@automatisch/types';
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
import qs from 'qs';
import { omitBy, isEmpty } from 'lodash';
const fetchTweets = async ($: IGlobalVariable) => {
const searchTweets = async ($: IGlobalVariable) => {
const searchTerm = $.step.parameters.searchTerm as string;
let response;
const tweets: ITriggerOutput = {
data: [],
};
do {
const params: IJSONObject = {
query: searchTerm,
@@ -30,14 +22,8 @@ const fetchTweets = async ($: IGlobalVariable) => {
response = await $.http.get(requestPath);
if (response.integrationError) {
tweets.error = response.integrationError;
return tweets;
}
if (response.data.errors) {
tweets.error = response.data.errors;
return tweets;
throw new Error(JSON.stringify(response.data.errors));
}
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);
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;

View File

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

View File

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

View File

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

View File

@@ -20,5 +20,23 @@ export const processFlow = async (options: ProcessFlowOptions) => {
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';
substeps: ISubstep[];
getInterval?(parameters: IGlobalVariable['step']['parameters']): string;
run($: IGlobalVariable): Promise<ITriggerOutput>;
run($: IGlobalVariable): Promise<void | ITriggerOutput>;
sort?($: IGlobalVariable): void | ITriggerOutput;
}
export interface IActionOutput {
@@ -279,11 +280,15 @@ export type IGlobalVariable = {
id: string;
testRun: boolean;
};
output: {
data: ITriggerDataItem[];
error?: IJSONObject;
}
process?: (triggerDataItem: ITriggerDataItem) => Promise<void>;
};
declare module 'axios' {
interface AxiosResponse {
integrationError?: IJSONObject;
httpError?: IJSONObject;
}
}