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-key replaces it; the old one stops working at once;
  • an origin allow list: batches from other origins are rejected. Without origins it is https://*.box.parallelsandbox.com, which covers pages opened through a box's URLs (sceneUrl and the URLs of its web services). A list you pass replaces that default, so include it if you want both. Entries are scheme://host[:port], https://*.example.com, null or * (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.log is sent as info, 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 at localhost inside the box has no box id unless you pass boxId.

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.