Skip to content

Commit c2b3cdf

Browse files
authored
fix: add webiny watch timeout-related error reporting (#4647)
1 parent 51b498c commit c2b3cdf

File tree

15 files changed

+230
-159
lines changed

15 files changed

+230
-159
lines changed

packages/app/src/plugins/NetworkErrorLinkPlugin.ts

Lines changed: 0 additions & 23 deletions
This file was deleted.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React from "react";
2+
import { ApolloLinkPlugin } from "./ApolloLinkPlugin";
3+
import { onError } from "apollo-link-error";
4+
import { type ServerError } from "apollo-link-http-common";
5+
import { print } from "graphql/language";
6+
import createErrorOverlay from "./NetworkErrorLinkPlugin/createErrorOverlay";
7+
import { LocalAwsLambdaTimeoutMessage } from "./NetworkErrorLinkPlugin/LocalAwsLambdaTimeoutMessage";
8+
import { boolean } from "boolean";
9+
import { config as appConfig } from "~/config";
10+
import ErrorOverlay from "~/plugins/NetworkErrorLinkPlugin/ErrorOverlay";
11+
import GqlErrorOverlay from "./NetworkErrorLinkPlugin/GqlErrorOverlay";
12+
13+
const isLocalAwsLambdaFnInvocationTimeoutError = (error: any): error is ServerError => {
14+
return error.result && error.result.code === "LOCAL_AWS_LAMBDA_TIMEOUT";
15+
};
16+
17+
/**
18+
* This plugin creates an ApolloLink that checks for `NetworkError` and shows an ErrorOverlay in the browser.
19+
*/
20+
export class NetworkErrorLinkPlugin extends ApolloLinkPlugin {
21+
public override createLink() {
22+
return onError(({ networkError, operation }) => {
23+
const debug = appConfig.getKey("DEBUG", boolean(process.env.REACT_APP_DEBUG));
24+
25+
if (networkError && debug) {
26+
if (isLocalAwsLambdaFnInvocationTimeoutError(networkError)) {
27+
createErrorOverlay({
28+
element: (
29+
<ErrorOverlay
30+
message={<LocalAwsLambdaTimeoutMessage />}
31+
closeable={false}
32+
/>
33+
),
34+
closeable: false
35+
});
36+
return;
37+
}
38+
39+
createErrorOverlay({
40+
element: (
41+
<GqlErrorOverlay
42+
query={print(operation.query)}
43+
networkError={networkError}
44+
/>
45+
)
46+
});
47+
}
48+
49+
// TODO: also print graphQLErrors
50+
});
51+
}
52+
}

packages/app/src/plugins/NetworkErrorLinkPlugin/ErrorOverlay.tsx

Lines changed: 17 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,46 @@
11
import React, { useState } from "react";
2-
import get from "lodash/get";
32
import { IconButton } from "@webiny/ui/Button";
43
import { Typography } from "@webiny/ui/Typography";
54
import { i18n } from "../../i18n";
6-
import { OverlayWrapper, Pre } from "./StyledComponents";
5+
import { OverlayWrapper } from "./StyledComponents";
76
import { ReactComponent as CloseIcon } from "./assets/close_24px.svg";
87

98
const t = i18n.ns("app/graphql/error-overlay");
109

1110
const ENVIRONMENT_VARIABLES_ARTICLE_LINK =
1211
"https://www.webiny.com/docs/how-to-guides/environment-variables";
1312

14-
interface ErrorOverlayProps {
15-
query: string;
16-
networkError: {
17-
message: string;
18-
result?: {
19-
error?: {
20-
stack?: string;
21-
};
22-
};
23-
};
24-
}
13+
type ErrorOverlayProps = Partial<{
14+
title: React.ReactNode;
15+
message: React.ReactNode;
16+
description: React.ReactNode;
17+
closeable?: boolean;
18+
}>;
19+
2520
const ErrorOverlay = (props: ErrorOverlayProps) => {
26-
const { query, networkError } = props;
21+
const { title = "An error occurred", message, description, closeable } = props;
2722
const [open, setOpen] = useState(true);
28-
// Log error in browser's developer console for further inspection.
29-
console.error({ networkError });
30-
3123
if (!open) {
3224
return null;
3325
}
3426

35-
const stackTrace = get(networkError, "result.error.stack");
36-
3727
return (
3828
<OverlayWrapper>
3929
<div className="inner">
4030
<div className="header">
4131
<div className="header__title">
42-
<Typography use={"headline4"}>{networkError.message}</Typography>
32+
<Typography use={"headline4"}>{title}</Typography>
4333
</div>
44-
<div className="header__action">
45-
<IconButton icon={<CloseIcon />} onClick={() => setOpen(false)} />
46-
</div>
47-
</div>
48-
<div className="body">
49-
<div className="body__summary">
50-
<Typography
51-
use={"subtitle1"}
52-
>{t`Error occurred while executing operation:`}</Typography>
53-
<Pre>
54-
<code>{query}</code>
55-
</Pre>
56-
</div>
57-
{stackTrace && (
58-
<div className="body__description">
59-
<Typography use={"subtitle1"}>{t`Complete stack trace:`}</Typography>
60-
<Pre>
61-
<code>{stackTrace}</code>
62-
</Pre>
34+
{closeable !== false && (
35+
<div className="header__action">
36+
<IconButton icon={<CloseIcon />} onClick={() => setOpen(false)} />
6337
</div>
6438
)}
6539
</div>
40+
<div className="body">
41+
<div className="body__summary">{message}</div>
42+
{description && <div className="body__description">{description}</div>}
43+
</div>
6644
<div className="footer">
6745
<Typography use={"body2"}>
6846
{t`
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import React from "react";
2+
import get from "lodash/get";
3+
import { Typography } from "@webiny/ui/Typography";
4+
import { i18n } from "../../i18n";
5+
import { Pre } from "./StyledComponents";
6+
import ErrorOverlay from "./ErrorOverlay";
7+
8+
const t = i18n.ns("app/graphql/error-overlay");
9+
10+
interface ErrorOverlayProps {
11+
query: string;
12+
networkError: {
13+
message: string;
14+
result?: {
15+
error?: {
16+
stack?: string;
17+
};
18+
};
19+
};
20+
}
21+
22+
const GqlErrorOverlay = (props: ErrorOverlayProps) => {
23+
const { query, networkError } = props;
24+
const stackTrace = get(networkError, "result.error.stack");
25+
26+
const title = networkError.message;
27+
28+
const message = (
29+
<>
30+
<Typography
31+
use={"subtitle1"}
32+
>{t`Error occurred while executing operation:`}</Typography>
33+
<Pre>
34+
<code>{query}</code>
35+
</Pre>
36+
</>
37+
);
38+
39+
let description: React.ReactNode = null;
40+
if (stackTrace) {
41+
description = (
42+
<>
43+
<Typography use={"subtitle1"}>{t`Complete stack trace:`}</Typography>
44+
<Pre>
45+
<code>{stackTrace}</code>
46+
</Pre>
47+
</>
48+
);
49+
}
50+
51+
return <ErrorOverlay title={title} message={message} description={description} />;
52+
};
53+
54+
export default GqlErrorOverlay;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import React from "react";
2+
import styled from "@emotion/styled";
3+
4+
const Code = styled.code`
5+
font-family: monospace;
6+
font-size: 0.85rem;
7+
line-height: 1rem;
8+
`;
9+
10+
export const LocalAwsLambdaTimeoutMessage = () => (
11+
<>
12+
Local AWS Lambda function execution timed out.
13+
<br />
14+
<br />
15+
Did you stop the&nbsp;
16+
<a
17+
href={"https://www.webiny.com/docs/core-development-concepts/basics/watch-command"}
18+
rel={"noreferrer noopener"}
19+
target={"_blank"}
20+
>
21+
<Code>webiny watch</Code>
22+
</a>
23+
&nbsp;command? If so, please restart the command or deploy your changes via the&nbsp;
24+
<a
25+
href={"https://www.webiny.com/docs/core-development-concepts/basics/project-deployment"}
26+
rel={"noreferrer noopener"}
27+
target={"_blank"}
28+
>
29+
<Code>webiny deploy</Code>
30+
</a>
31+
&nbsp;command. For example: <Code>yarn webiny deploy api --env dev</Code>.
32+
<br />
33+
<br />
34+
Learn more:&nbsp;
35+
<a
36+
href={"https://webiny.link/local-aws-lambda-development"}
37+
rel={"noreferrer noopener"}
38+
target={"_blank"}
39+
>
40+
https://webiny.link/local-aws-lambda-development
41+
</a>
42+
</>
43+
);

packages/app/src/plugins/NetworkErrorLinkPlugin/createErrorOverlay.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,14 @@ import React from "react";
44
*/
55
// eslint-disable-next-line react/no-deprecated
66
import { render } from "react-dom";
7-
import ErrorOverlay from "./ErrorOverlay";
8-
import { ServerError, ServerParseError } from "apollo-link-http-common";
97

108
interface CreateErrorOverlayParams {
11-
query: string;
12-
networkError: Error | ServerError | ServerParseError;
9+
element: React.ReactElement;
10+
closeable?: boolean;
1311
}
12+
1413
const createErrorOverlay = (params: CreateErrorOverlayParams): void => {
15-
const { query, networkError } = params;
14+
const { element } = params;
1615
// If the element already present in DOM, return immediately.
1716
if (document.getElementById("overlay-root")) {
1817
return;
@@ -24,7 +23,7 @@ const createErrorOverlay = (params: CreateErrorOverlayParams): void => {
2423
const body: HTMLBodyElement = document.getElementsByTagName("body")[0];
2524
body.appendChild(container);
2625
// Mount the ErrorOverlay component into root element.
27-
render(<ErrorOverlay query={query} networkError={networkError} />, container);
26+
render(element, container);
2827
};
2928

3029
export default createErrorOverlay;

packages/cli-plugin-deploy-pulumi/src/commands/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,11 @@ export const commands: CliCommandPlugin[] = [
251251
describe: `Increase AWS Lambda function timeout (passed as number of seconds, used with local AWS Lambda development)`,
252252
type: "number"
253253
});
254+
yargs.option("increase-handshake-timeout", {
255+
default: 5,
256+
describe: `Increase timeout for the initial handshake between a single AWS Lambda invocation and local code execution (passed as number of seconds, used with local AWS Lambda development)`,
257+
type: "number"
258+
});
254259
yargs.option("allow-production", {
255260
default: false,
256261
describe: `Enables running the watch command with "prod" and "production" environments (not recommended).`,

packages/cli-plugin-deploy-pulumi/src/commands/newWatch.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ export const newWatch = async (inputs: IUserCommandInput, context: Context) => {
181181
});
182182
const sessionId = new Date().getTime();
183183
const increaseTimeout = inputs.increaseTimeout;
184+
const localExecutionHandshakeTimeout = inputs.increaseHandshakeTimeout || 5; // Default to 5 seconds.
184185

185186
// We want to ensure a Pulumi refresh is made before the next deploy.
186187
setMustRefreshBeforeDeploy(context);
@@ -196,7 +197,8 @@ export const newWatch = async (inputs: IUserCommandInput, context: Context) => {
196197
iotEndpointTopic,
197198
sessionId,
198199
functionsList,
199-
increaseTimeout
200+
increaseTimeout,
201+
localExecutionHandshakeTimeout
200202
});
201203

202204
let inspector: typeof inspectorType | undefined = undefined;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
## About
2+
3+
This folder contains the AWS Lambda function code that gets uploaded by the `webiny watch` command.
4+
5+
The code basically enables forwarding of events from AWS Lambda to the `webiny watch` command, which then runs the actual function code in the local environment.
6+
7+
Learn more: https://webiny.link/local-aws-lambda-development.
8+
9+
## Development
10+
11+
In this folder, you can find the `handler.zip` file, which is what the `webiny watch` command uploads to AWS Lambda.
12+
13+
In the archive, you will find the following files:
14+
1. `handler.js` - the main handler code that processes incoming events and forwards them to the local environment
15+
2. `mqtt.js` - a utility for handling MQTT messages (already prepared for direct import in `handler.js`, no NPM install needed).
16+
17+
In case you need to update the handler code, you must manually unzip the file, make the changes, and then zip it again.
18+
19+
To test the changes, build the `@webiny/cli-plugin-deploy-pulumi` package and run the `webiny watch` command. The command will automatically upload the new `handler.zip` file to AWS Lambda.
20+
21+
Repeat the process until you are satisfied with the results.
22+
23+
Note that, to build the `@webiny/cli-plugin-deploy-pulumi` package, run the following command from the root of the Webiny project:
24+
25+
```bash
26+
yarn build -p @webiny/cli-plugin-deploy-pulumi
27+
```
28+
### Development via AWS Lambda Console
29+
30+
Another way to iterate on the code a bit faster is to simply use the AWS Lambda console directly.
31+
32+
Just run the `webiny watch` command, and then open the AWS Lambda console and find the function you wish to test your changes with. You can then edit the `handler.js` file directly in the console. Once you're done with, just c/p the code from the console and paste it into the `handler.js` file in this folder, and then zip it again.
33+

0 commit comments

Comments
 (0)