Log SDK
@parallelsandbox/log is a small browser SDK. Add it to any page you run inside a box (or anywhere else) and its console output and errors land in a log project you own. Your agent then reads them with logs_search, logs_errors and logs_tail instead of asking you to paste console output.
It runs in browsers only: it hooks the page's console and window events, and the log service accepts a batch only from an origin the project allows. It is not a server-side logger. For programs running in the box itself, read their output directly (see Without the log service).
Projects and keys
A log project is created over REST, with a token, an API key or an OAuth access token (where keys come from); do it from your own machine, not from a box:
curl -fsS -X POST https://log.parallelsandbox.com/v1/projects \
-H "Authorization: Bearer $PSBX_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "name": "example-web", "origins": ["http://localhost:8080", "https://*.box.parallelsandbox.com"] }'
The response is {"project": {"id": "prj_...", ...}, "writeKey": "psw_..."}. A project has:
- a project id,
prj_..., used in every query (the tools also accept the project's name); - a write key,
psw_..., which the browser sends with every batch. It is shown only in this response. It can only write into this project, so it is safe to put in a page.POST /v1/projects/{id}/write-keyreplaces it; the old one stops working at once; - an origin allow list: batches from other origins are rejected. Without
originsit ishttps://*.box.parallelsandbox.com, which covers pages opened through a box's URLs (sceneUrland the URLs of its web services). A list you pass replaces that default, so include it if you want both. Entries arescheme://host[:port],https://*.example.com,nullor*(any origin, also requests without one); at most 32.PATCH /v1/projects/{id}with{"origins": [...]}changes it; - a rate limit per project and per sending address.
GET /v1/projects lists your projects; DELETE /v1/projects/{id} deletes one with its logs and source maps. Each project is its own log group. Logs are kept without expiry until the project is deleted; deleting it ends its log and source map storage charges.
Install
Use 0.1.2 or later. It reads the box id from the host of any of a box's URLs, <id>-<key>.box.parallelsandbox.com (sceneUrl, also the url of the first declared service when it is web) and <id>-<key>-<targetPort>.box.parallelsandbox.com (the url of a later web service), and never sends the key: every <id>-<key> in a message, stack, route or extra becomes <id> before the batch leaves the page, so https://<id>-<key>-5173.box.parallelsandbox.com/app.js is recorded as https://<id>-5173.box.parallelsandbox.com/app.js. The log service does the same on arrival. Earlier versions do not read the box id from a web service's URL.
From npm, for bundlers:
npm install @parallelsandbox/log@^0.1.2
Or without a bundler, through an import map (@0.1 is the latest 0.1.x):
<script type="importmap">
{ "imports": { "@parallelsandbox/log": "https://cdn.jsdelivr.net/npm/@parallelsandbox/log@0.1/+esm" } }
</script>
Initialise
import { init } from '@parallelsandbox/log';
const log = init({
writeKey: 'psw_...', // the project's write key; it names the project
release: 'a1b2c3d', // build id; must match the release you upload source maps under
});
Call it once, as early as possible. init throws if writeKey does not start with psw_; calling it again replaces the previous logger. Options:
| Option | Default | Meaning |
|---|---|---|
writeKey |
required | the project's write key |
endpoint |
https://log.parallelsandbox.com |
log service |
release |
empty | build id; stacks are resolved only when it matches uploaded source maps |
boxId |
from the page host | box id; read from the host of a box's URL (<id>-<key>.box.parallelsandbox.com or <id>-<key>-<targetPort>.box.parallelsandbox.com), empty elsewhere |
boxDomain |
box.parallelsandbox.com |
domain of box hosts |
captureConsole |
true |
hook console.debug, log, info, warn and error |
captureErrors |
true |
capture window errors and unhandled promise rejections |
levels |
['info', 'warn', 'error'] |
levels to send; add 'debug' to send console.debug |
flushIntervalMs |
2000 |
time between sends |
batchSize |
50 |
entries per request, at most 200 |
maxBuffer |
1000 |
entries kept in memory; newer ones are dropped beyond it |
platform |
'web' |
label sent with every batch |
After init:
console.logis sent asinfo, the other console methods as their own level; an uncaught error becomes[uncaught] <message> at <file>:<line>:<column>with its stack, and an unhandled rejection[unhandledrejection] <message>.- Entries are batched and sent every
flushIntervalMs, and what is left is sent when the page is hidden or unloaded. - Every batch carries
release,platform, the user agent, the page's path and query (route) and the box id. When the page is served through one of the box's URLs, the box id comes from its host and the key never leaves the page: every<id>-<key>in the batch becomes<id>, and the log service does the same on arrival and takes the box id from the page's origin. A page opened atlocalhostinside the box has no box id unless you passboxId.
Manual calls
init returns the logger:
log.info('checkout started', { cart: 3 });
log.warn('slow response', { ms: 2400 });
try {
riskyThing();
} catch (err) {
log.error(err, { step: 'riskyThing' }); // an Error keeps its stack
}
await log.flush(); // before navigating away in a test
debug, info and warn take a message and an optional extra object; error takes an Error or any value and an optional extra. extra is any JSON-serialisable object and is searchable with logs_search. The logger also has setBox(id), setRelease(release), boxId and uninstall(), which restores console and the window listeners.
Source maps
Minified stacks are useless to an agent. Upload the source maps of every build under the same release you pass to init, and logs_errors returns stacks resolved to original files and lines.
curl -fsS -X POST https://log.parallelsandbox.com/v1/projects/prj_.../sourcemaps \
-H "Authorization: Bearer $PSBX_TOKEN" \
-F "release=a1b2c3d" \
-F "assets/app-ALQGRFWG.js.map=@dist/assets/app-ALQGRFWG.js.map;type=application/json" \
-F "assets/log-init-K6FPZHJN.js.map=@dist/assets/log-init-K6FPZHJN.js.map;type=application/json"
Each map is one file part, and the part's name is the path the map is served at, relative to the page origin (the script in the stack plus .map); one request can carry every map of a release. Uploading uses the account's token, not the write key. GET /v1/projects/{id}/sourcemaps?release= lists what was uploaded. example-web-with-logs has a script that does this for a whole dist/ directory.
A stack is resolved once, when the error arrives, for error-level entries that have a stack and a release. Upload the maps before the page runs; errors that arrived earlier keep their minified stack.
Query from the agent
logs_errors { "project": "prj_...", "since": "30m", "boxId": "<box id>" }
logs_search { "project": "prj_...", "since": "1h", "query": "checkout" }
logs_tail { "project": "prj_...", "boxId": "<box id>" }
Rows have ts, ingestedAt, level, box, release, platform, route, msg, stack, stackResolved, extra and stream. logs_search and logs_errors return them oldest first with a nextCursor; logs_tail is a long poll that returns when new rows arrive or after waitSec, with a cursor for the next call. Parameters are in the tool reference.
Cost
Log ingestion and retention are metered like everything else:
| Usage | Credits |
|---|---|
| Written | 760 per GB |
| Stored | 33 per GB per month |
A page that reports a few hundred errors a day costs well under one credit a month.