Skip to content
This repository has been archived by the owner on Jan 17, 2023. It is now read-only.

Commit

Permalink
Merge pull request #2349 from mozilla-services/architecture-documents
Browse files Browse the repository at this point in the history
Architecture documents
  • Loading branch information
ianb authored Mar 10, 2017
2 parents 1d19fe4 + 5ee3352 commit 837195e
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 62 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ If you have growl and growlnotify installed on Mac OS X, you will get growl noti

We apologize but we have no story for development on Windows (though the add-on runs on Windows). We welcome feedback.

### Getting to know the code

There is documentation in [addon/](https://github.com/mozilla-services/pageshot/blob/master/addon/), [addon/webextension/](https://github.com/mozilla-services/pageshot/blob/master/addon/webextension/), [addon/webextension/background/](https://github.com/mozilla-services/pageshot/blob/master/addon/webextension/background/), and [addon/webextension/selector/](https://github.com/mozilla-services/pageshot/blob/master/addon/webextension/selector) that talks about the code layout and architecture of the add-on.

[server/view-docs.md](https://github.com/mozilla-services/pageshot/blob/master/server/views-docs.md) talks about how the server React pages are setup, along with the server-side rendering of pages.

There is also documentation in [docs/](https://github.com/mozilla-services/pageshot/blob/master/docs/).

### Participation

There is an IRC channel `#pageshot` on irc.mozilla.org (you can use [this link](https://kiwiirc.com/client/irc.mozilla.org/pageshot) for chat access via the web if you do not otherwise use IRC). There are [IRC logs available](http://logs.glob.uno/?c=pageshot).
Expand Down
17 changes: 17 additions & 0 deletions addon/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
This is the root of the add-on. This add-on is an [Embedded Extension](https://developer.mozilla.org/Add-ons/WebExtensions/Embedded_WebExtensions), which means we have a [Bootstrap Extension](https://developer.mozilla.org/Add-ons/Bootstrapped_extensions) that wraps a [WebExtension](https://developer.mozilla.org/Add-ons/WebExtensions).

The bootstrap extension lives in this directory (in `bootstrap.js`), along with `install.rdf` that describes this add-on. The WebExtension lives in `webextension/`. We try to do everything we can in the WebExtension and only do things in bootstrap.js that can't be done from a WebExtension.

Note in normal development you don't need to run the bootstrap portion. That is, if you run:

```sh
$ ./bin/run-addon
```

then only the WebExtension will be run. This is easier to debug, and supports reloading. If you are developing something in `bootstrap.js` you must run:

```sh
$ ./bin/run-addon --bootstrap
```

You have to hit ^C and restart if you change code.
31 changes: 30 additions & 1 deletion addon/webextension/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
This contains all the files for the WebExtension. Most files are not "built", but a few files in `build/` are built and kept fresh automatically by `./bin/run-addon`

The organization:
## The Organization:

- Every file exports one object, named after the file (directory names ignored). The one object may be a function. File case should match the object case. Typically a mixed-case filename exports a function, while modules are exported in all-lowercase.
- There are three workers/processes:
Expand All @@ -17,3 +17,32 @@ Note that shared files are located directly in this directory. These files shou
- `randomString.js` generates a random string, typically for in shot IDs.
- `domainFromUrl.js` gets the domain from a URL (e.g., `"google.com"` from `"https://google.com/"`)
- `makeUuid.js` generates a UUID

## Communication:

To support communication, `background/communication.js` handles incoming messages, and `selector/callBackground.js` handles sending messages.

The basic flow:

1. Everything starts when the button is clicked. This fires an event in `background/main.js`
2. The background page loads the content worker with `background/loadSelector.js`
3. `selector/shooter.js` handles most communication logic from the selector side
4. `shooter.js` collects the information and creates a Shot object.
5. `selector/uicontrol.js` handles the UI logic, button handlers, selection process, etc.
6. When you hit **Download** and [canvas.drawWindow](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawWindow) is supported, then the `data:` URL is created immediately, an `<a download="filename" href="data:...">` link is created and synthetically clicked.
7. When you hit **Download** and `canvas.drawWindow` isn't supported, then we send a message to ask the background page to capture the selection, turn it into a data URL, return the data URL, and then we continue with creating the anchor.
8. If you hit **Cancel** then the content-worker self-destructs
9. If you hit **Save**, **Full Page** or **Visible Page** then the logic is the same – we define a rectangle based on the button, and continue.
10. The UI is hidden.
11. If `canvas.drawWindow` is supported then the content worker adds an image to the shot.
12. The shot is sent to the background page, along with the selection rectangle.
13. If the background page sees there's no image (presumably `canvas.drawWindow` isn't supported), it uses `captureVisibleTab` to add an image.
14. The authentication information is fetched (`background/auth.js`). If the client authenticated earlier in the session then we return that information.
15. If the client isn't authenticated then we get the registration/authentication information from `browser.storage`.
16. If there's no authentication information then we create it (it's a random ID and secret).
17. The background page calls `/api/#` or `/api/register` if the login 404s. It gets back an authentication header.
18. Now we have the authentication header, and can continue saving the shot.
19. The shot is submitted to the server with the authentication header.
20. We open a new tab with `/creating?...` immediately, before the save is completed.
21. Once the shot has been uploaded successfully we take that created tab and navigate to the shot page.
22. The link is copied to the clipboard and a notification is popped up telling the user their shot was created.
1 change: 1 addition & 0 deletions docs/METRICS.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ The primary change was in `server/src/pages/shot/share-buttons.js`
7. [ ] Cancel because the tab is navigated (such as entering something in the URL bar) `addon/cancel-shot/tab-load` (FIXME: need to track)
8. [ ] Cancel because the tab is reloaded `addon/cancel-shot/tab-reload` (FIXME: need to track)
5. [x] Click My Shots `addon/goto-myshots/selection-button`
6. [x] Go to My Shots by hitting the Page Shot button on a about:newtab page `addon/goto-myshots/about-newtab`
6. [x] Click on "Save visible" `addon/capture-visible/selection-button`
7. [x] Click on "Save Full Page" `addon/capture-full-page/selection-button`
6. ~~Click My Shots button from error panel `addon/goto-myshots/error-panel`~~
Expand Down
132 changes: 71 additions & 61 deletions docs/error-handling.md
Original file line number Diff line number Diff line change
@@ -1,97 +1,107 @@
# Error Handling

Specifically in the addon it is challenging to handle error handling in all the many execution environments that exist. To handle this we need to be careful to catch errors wherever they happen and let them emerge in some sensible way.
In Page Shot we try to capture all unexpected failures for the purpose of reporting (and hopefully for fixing!)

Errors should take the form of:
## Catching

```javascript
{
name: "ERROR_NAME",
message: "description for programmer",
help: "description for the user",
helpHtml: "rich description for the user"
}
```
To do this we have to wrap anything that *won't* report the error. That means:

The `name` should be stable, and is what you might check against when you think you may be able to handle some kinds of errors. The `help` and optional `helpHtml` keys are something that can be shown to the user. These may be useful to rewrite as errors are caught and re-raised.
* Any callbacks or event listeners
* Any promises that aren't returned and where there's not another explicit `.catch()` handler

These error objects must frequently cross process boundaries, so we can't expect any special object representation, only a raw JSONable object.
Not everything has to be wrapped! You can assume that any function you write in Page Shot will normally be called by another Page Shot function, and it's the caller's responsibility to catch an exception. Similarly if you return a Promise, it is the caller's responsibility to catch errors from the Promise.

## Where to send errors
*At the point* where you pass a Page Shot function to some code that will call the function, but *doesn't* know how to report errors, then you have to wrap that function. Also, if you know a promise won't be returned and will be thrown away after it completes, you must wrap that promise.

Errors should all propagate to the addon. (Question: should they? Alternately some could go to the addon, and others could be handled in content when you are viewing a pageshot page.)
Also, if there are recoverable but unexpected errors you can report an error explicitly.

Errors come in in several ways:
### The `catcher` module

### Content
The [catcher](../addon/webextension/catcher.js) module is loaded in both the content/worker process and in the background page. Errors from the content process are sent to the background page, but this should be transparent to you.

Content that has a helper associated with it should call:
#### `catcher.watchFunction`

```javascript
var event = document.createEvent("CustomEvent");
event.initCustomEvent("error", true, true, errorObject);
document.dispatchEvent(event);
```
If you have a function that should be watched, use `catcher.watchFunction(func)`:

A page worker should be attached to the page that will catch that event and route it to the addon.
```js
document.addEventListener("click", catcher.watchFunction(myCallback));
```

### framescript
This wraps the function (but does not call it!) so that any exceptions are reported and then re-thrown.

A framescript should return a value like:
Note that this will erase implicit `this` bindings, so you may need to do:

```javascript
sendAsyncMessage("pageshot@messageName:return", {
error: {
name: "ERROR", ...
}
});
```js
document.addEventListener("click", catcher.watchFunction(this.onClick.bind(this)));
```

This should be handled by `lib/framescripter.js`
Also note that this changes the identity of a function, so sometimes you have to be even more explicit:

```js
let watchedMyCallback = catcher.watchFunction(myCallback);
document.addEventListener("click", watchedMyCallback);
// later...
document.removeListener("click", watchedMyCallback);
```

### worker
#### `catcher.watchPromise`

An addon worker should do:
If something is the last caller of a promise, then it should watch any promises. For example:

```javascript
self.port.emit("alertError", {name: "ERROR", ...});
```js
function myCallback() {
catcher.watchPromise(startSomething().then(() => {
return nextThing();
}).then((result) => {
if (! result.ok) {
throw new Error("Something went wrong!");
}
}));
}
```

### Addons
Put `catcher.watchPromise()` around the entire promise chain. Throw errors in any handler and it will get reported.

If code in an addon receives an error and cannot handle it, it should call:
#### `catcher.unhandled`

```javascript
require("./errors").unhandled(errorObject);
If you want to report an error but continue on, you should use something like this:

```js
function doStuff(foo) {
try {
someOtherFunction({context: foo});
} catch (e) {
if (e.name == "STRANGE_ERROR") {
catcher.unhandled(e, {context: foo});
}
}
}
```

Generally you should use `watchPromise()` if you call `promise.then(success)` and don't include any failure handler. You should call `watchWorker(worker)` on every worker you create. And you should add `watchFunction()` around any function that is called in an event handler. These are all examples of cases when your code is either being called by something that won't handle errors (like a Jetpack event invoker), or we need to wire up processes, or we are clearly ignoring a value.
If you don't have an exception object, create a new one with `new Error("Error Name")`

Generally if you return a promise, you don't need this error handling if you simply call `.reject()` properly. We should be on the lookout for cases when a promise return value is entirely ignored, as that's harder to detect than an incomplete invocation of `.then(success)`
The second argument is additional information you can add to the report.

## Helpers
## Formatting a good error

Everything described here can be a bit challenging. Some helpers exist:
Each error should be an exception. If you get a natural exception then use that, but if you are creating one:

```javascript
const { watchPromise, watchFunction, watchWorker, watchRun } = require("./errors");
**Use a fixed error string.** E.g., `new Error("Response failed")`, not `new Error("Error for " + url + " failed")`. Sentry groups errors based on the exception string.

// Adds a .reject() handler to the promise
watchPromise(deferred.promise);
**Use an error object**, not a string. I.e., never do `throw "there was an error"`. This is always good practice!

// Wraps the function (preserving this and arguments), catches any errors, and if
// a promise is returned it wraps the promise
{
method: watchFunction(function () {
return deferred.promise;
})
}
**Add extra information**, either to the exception object, or as a second argument when calling `catcher.unhandled(exc, extraInfo)`.

// Watches the worker, listening for `worker.port.on("error")`, returns the worker
watchWorker(worker);
When adding information to an exception object, simply add attributes, such as:

// Executes the function immediately, catching any errors:
watchRun(function () {
// ...
}, this);
```js
let exc = new Error("Error in request");
exc.responseStatusCode = req.status;
exc.headerNames = Object.keys(headers);
```

Note that any information will be serialized as JSON. Specifically `undefined` will be lost in JSON serialization.

If you add `exc.popupMessage = "Something happened"` then that detail will be added. Be careful about localization here.

Add `exc.noPopup = true` if you don't want the user notified about the error (but the error will still be sent to Sentry).

0 comments on commit 837195e

Please # to comment.