Fruitback

What the widget collects

The widget runs on a page somebody else visits, and what they write leaves that page. If your team is in the European Union, or your reporters are, that is a processing of personal data and you are the one who has to declare it. You cannot do that without a list of what is sent, so this page is the list.

This is not legal advice. It says what the code does, field by field, and what it does not do yet. What you owe your reporters under the law that applies to you is for you, or your counsel, to decide.

What a note carries

A note is one seed: a JSON object the widget builds in the reporter’s browser and posts to your worker. Its shape is seedSchema in packages/shared/src/seed.ts, and nothing outside that schema is sent.

Field What it is Sent What controls it
note what the reporter typed, up to 5,000 characters always the reporter
page.url, page.path the page address, with the fragment and the tracking parameters removed always nothing: the page address is how a pin is found again
page.title the page’s <title> when the page has one the site
viewport the window’s width, height and pixel ratio always nothing
anchor a selector, a structural path, the tag, up to 160 characters of the element text always nothing: this is what places the pin
anchor.attrs the element’s id, test id, name, role and aria-label, when present when the element has them the site’s markup
source the React component, file, line and column behind the element when the build exposes them the site’s build: a production build usually exposes none
client.id the client id the site was given always the integrator
reporter.name what the reporter typed in the optional name field only if the reporter fills it in the reporter
reporter.id, .name, .email the identity in a signed token, replacing anything typed added by the worker, from a valid token the integrator (identityToken) or, in team mode, the pairing
env.userAgent, env.locale, .platform the browser’s user agent string, language and platform only if the integrator turns it on the integrator: init({ includeEnv: true }), or the attribute on the tag
screenshot the URL of a picture of the page only if the reporter turns it on the integrator supplies captureScreenshot; the reporter switches it on
id, createdAt a random identifier and the time of writing always nothing

The excerpt of the element’s text (anchor.text) is the site’s own text, not the reporter’s. It can still be personal data: an element that shows a customer’s name carries that name into the seed. If your staging site shows real customer records, keep that in mind before you invite reviewers.

What is right by default

What you can turn on

Where it goes

The widget talks to one address: the worker the integrator, or the reviewer in the extension, named. Fruitback runs no service of its own and receives nothing. The worker then writes the seed to the store it is configured with:

Store Where a note lands Who runs it
linear an issue in your Linear workspace, the seed in its body Linear, outside your infrastructure
github an issue in your GitHub repository, the seed in its body GitHub, outside your infrastructure
sqlite a row in a database file on your volume you

In the two trackers, the issue description also states the reporter’s name and e-mail in a line your team reads, and the seed below it repeats them. SECURITY.md says who can read each store. On a public GitHub repository, that is everyone.

Who can read a note back

The widget reads the notes of a page to draw their pins, and the answer carries the whole seed, name and e-mail included, and the replies of your team with their authors’ names. By default the read path is public (FRUITBACK_READ=public): anybody who knows a page address and its client id can read them, with or without the widget.

What the worker keeps besides the note

What stays in the reporter’s browser

How long a note is kept

As long as the issue or the row exists. Fruitback deletes nothing on its own.

Deleting a reporter’s notes

The widget asks for no e-mail address since FRU-91, so a note carries one in two cases only: the integrator’s identity token gave it, or the note was written before. A typed name is a claim, and so was a typed e-mail: a reporter can write somebody else’s, and two reporters can write the same one. So list the notes first, read the list, and delete after.

A note signed with a name only has no address to search for, and a note with no name has nothing to search for at all. Each note has an identifier, and the thread of its pin shows it on the page.

If you delete SQLite rows by hand with sqlite3, start the session with PRAGMA foreign_keys = ON. The sqlite3 shell turns foreign keys off for each new connection, and without them the note goes and its replies stay, attached to nothing (measured). The worker’s own connection has them on.

A notice you can adapt

Put this near the widget, or in your own privacy policy, and change what is not true for your deployment:

Feedback on this page. When you leave a note, we receive what you write, the address of this page, the part of the page you pointed at, and the size of your window. Your name is optional; we receive it only if you type it, and your browser keeps it only if you ask it to. A picture of the page is sent only if you turn it on. Your note is stored in [Linear / GitHub / our own server] and is visible to [our team / anyone who can open this page] until we delete it. To have your notes erased, write to [address].

If you turned includeEnv on, add “and your browser’s name, version and language” to the first sentence. Remove the picture sentence if you supplied no captureScreenshot.