Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
576 changes: 576 additions & 0 deletions docs/superpowers/plans/2026-07-22-web-extension-cloud-upload.md

Large diffs are not rendered by default.

115 changes: 115 additions & 0 deletions docs/superpowers/specs/2026-07-22-web-extension-cloud-upload-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Web Extension Cloud Upload Design

## Goal

Add completed-session uploads to the rrweb browser extension without changing
recording startup or session identity behavior. The extension will upload to
`https://api.rrweb.com` by default, while allowing users to configure another
HTTP or HTTPS API base URL.

## Scope

The feature includes:

- an Upload action for selected saved sessions;
- a settings screen for an API base URL and bearer token;
- local-only credential storage;
- NDJSON serialization with Brotli, gzip, and raw fallbacks;
- per-session success and error feedback; and
- automated tests for configuration, request construction, fallbacks, and
partial failures.

The feature deliberately excludes automatic recording, page-to-extension
session ID bridges, and any change to how recordings receive IDs. Those changes
belong in a separate pull request.

## Architecture

`src/utils/storage.ts` remains responsible only for IndexedDB session and event
storage. A new `src/utils/cloud-upload.ts` module owns configuration
normalization, NDJSON serialization, compression selection, URL construction,
and the HTTP request. This boundary keeps network behavior independently
testable and prevents storage code from accumulating transport concerns.

`src/options/Settings.tsx` reads and writes cloud configuration through
`Browser.storage.local`. The API token is never placed in sync storage. The API
base URL defaults to `https://api.rrweb.com`, is normalized by removing trailing
slashes, and must use HTTP or HTTPS. The upload endpoint is constructed as
`<baseUrl>/recordings/<encodedSessionId>/ingest`.

`src/pages/SessionList.tsx` retrieves selected session IDs, delegates them to
the upload module, and presents aggregate success or per-session failure
messages. It does not access credentials or construct network requests.

## Configuration and Credentials

The cloud settings shape is:

```ts
type CloudSettings = {
apiBaseUrl: string;
authToken: string;
};
```

Defaults are applied when settings are missing or incomplete. The token input
uses a password field. Saving an empty token is allowed, but upload attempts
fail before reading session payloads or issuing a request. No token or event
payload is written to logs.

HTTP is accepted to support local development; production users receive the
HTTPS default. Unsupported URL protocols and malformed URLs are rejected with
a settings validation error.

## Upload Data Flow

For each selected session, the upload module:

1. loads session metadata and recorded events;
2. serializes each event as one JSON line;
3. attempts Brotli compression when the runtime supports it;
4. falls back to gzip when Brotli is unavailable or fails;
5. falls back to the raw NDJSON string when compression is unavailable;
6. sends one POST request with `Authorization: Bearer <token>` and
`Content-Type: application/x-ndjson`;
7. includes `Content-Encoding` only for compressed bodies; and
8. records success or a concise error for that session before continuing.

Session IDs are URL-encoded. Non-success HTTP responses include status and
status text in the result without exposing response bodies that might contain
sensitive information.

## Error Handling

Missing credentials and invalid API URLs fail before network activity. Missing
session metadata or events fail only that session. Compression failures degrade
to the next supported encoding rather than aborting an upload. A failed request
does not prevent remaining selected sessions from being attempted.

The UI distinguishes complete success, partial failure, and total failure.
Errors remain visible through Chakra toasts and name the affected saved session.

## Testing

The extension package will gain a focused Vitest configuration and unit tests.
Pure transport helpers will cover:

- the `https://api.rrweb.com` default;
- trailing-slash normalization and encoded session IDs;
- rejection of malformed or unsupported URLs;
- missing-token short-circuiting;
- Brotli, gzip, and raw-body request headers;
- bearer authorization without credential logging;
- non-success HTTP responses; and
- continuation after individual session failures.

Verification will run the extension unit tests, TypeScript checking, and both
Chrome and Firefox production builds.

## Git Strategy

Implementation lives in the isolated worktree
`/Users/justin/.config/superpowers/worktrees/rrweb/web-extension-cloud-upload`
on branch `codex/web-extension-cloud-upload`. Only the curated upload/settings
changes will be ported; unrelated modifications from the source workspace will
not be copied.
23 changes: 23 additions & 0 deletions packages/web-extension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,29 @@ yarn dev:chrome
yarn dev:firefox
```

## Cloud uploads

Completed recordings stay in the extension's local session storage until you
select them and choose **Upload**. Uploading does not start recording
automatically, does not remove the local recording, and this feature does not
add a page-to-session-ID bridge.

Configure uploads from the extension's **Settings** page. The default API base
URL is `https://api.rrweb.com`; you can configure another base URL for a proxy
or local development. Remote endpoints must use HTTPS because each upload sends
the bearer token and recording data. Plain HTTP is only appropriate for trusted
local development. The authentication bearer token is stored only in
extension-local storage (`Browser.storage.local`) and is never synchronized.

For each selected session, the extension sends a `POST` request to
`<base URL>/recordings/<session ID>/ingest`. Its body is NDJSON event data, with
Brotli compression when available, gzip as a fallback, and an uncompressed
request as a last resort.

The configured endpoint must permit the extension origin to make `POST`
requests via CORS, including the `Authorization`, `Content-Type`, and
`Content-Encoding` request headers.

## Sponsors

[Become a sponsor](https://opencollective.com/rrweb#sponsor) and get your logo on our README on Github with a link to your site.
Expand Down
5 changes: 5 additions & 0 deletions packages/web-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@
"build:firefox": "cross-env TARGET_BROWSER=firefox vite build",
"pack:chrome": "cross-env TARGET_BROWSER=chrome ZIP=true vite build",
"pack:firefox": "cross-env TARGET_BROWSER=firefox ZIP=true vite build",
"test:unit": "vitest run --config vitest.config.ts",
"test:unit:watch": "vitest --config vitest.config.ts",
"check-types": "tsc -noEmit",
"build": "npm run pack:chrome && npm run pack:firefox",
"prepublish": "yarn build"
},
"devDependencies": {
"@rrweb/types": "^2.1.1",
"@testing-library/react": "^14.3.1",
"@testing-library/user-event": "^14.6.1",
"@types/chrome": "^0.0.287",
"@types/react-dom": "^18.0.6",
"@types/semver": "^7.5.8",
Expand All @@ -29,6 +33,7 @@
"vite": "^6.0.1",
"vite-plugin-web-extension": "^4.1.3",
"vite-plugin-zip-pack": "^1.2.2",
"vitest": "^1.4.0",
"webextension-polyfill": "^0.10.0"
},
"dependencies": {
Expand Down
3 changes: 2 additions & 1 deletion packages/web-extension/src/options/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Route, Routes } from 'react-router-dom';
import SidebarWithHeader from '~/components/SidebarWithHeader';
import { FiList, FiSettings } from 'react-icons/fi';
import { Box } from '@chakra-ui/react';
import { SettingsView } from './Settings';

export default function App() {
return (
Expand All @@ -23,7 +24,7 @@ export default function App() {
>
<Box p="10">
<Routes>
<Route path="/" element={<></>} />
<Route path="/" element={<SettingsView />} />
</Routes>
</Box>
</SidebarWithHeader>
Expand Down
Loading
Loading