Log SDK

@parallelsandbox/log 是一個很小的瀏覽器 SDK。掛在任何你在箱子裡(或其他地方)跑的網頁上,console 輸出與錯誤就會進到你自己的 log project。你的 agent 之後用 logs_search、logs_errors、logs_tail 讀,不用再叫你貼 console。

它只在瀏覽器裡跑:它掛的是頁面的 console 與 window 事件,log 服務也只收 project 允許的 origin 送來的批次。它不是伺服器端的 logger。箱子裡直接跑的程式,直接讀它們的輸出(見不用 log 服務時)。

Project 與 key

log project 用 REST 建,帶 token(API key 或 OAuth access token,key 從哪來);從你自己的機器建,不要在箱子裡:

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"] }'

回應是 {"project": {"id": "prj_...", ...}, "writeKey": "psw_..."}。每個 project 有:

  • project id,prj_...,每次查詢都帶(工具也接受 project 的名稱);
  • write key,psw_...,瀏覽器每批 log 都帶。只在這次回應裡出現。它只能寫這個 project,所以放在網頁上沒關係。POST /v1/projects/{id}/write-key 換一把新的,舊的立刻失效;
  • origin 白名單:其他來源送的一律拒收。沒給 origins 時是 https://*.box.parallelsandbox.com,涵蓋經箱子網址(sceneUrl 與它 web 服務的網址)打開的頁面。你給的清單會取代這個預設,兩種都要就一起列。每一項是 scheme://host[:port]、https://*.example.com、null 或 *(任何來源,連沒帶 origin 的請求也收),最多 32 項。用 PATCH /v1/projects/{id} 帶 {"origins": [...]} 修改;
  • 每個 project 與每個送出位址的限流。

GET /v1/projects 列出你的 project;DELETE /v1/projects/{id} 連同它的 log 與 source map 一起刪掉。每個 project 自己一個 log group。log 不設保留期限,會一直留到刪掉 project;刪掉之後 log 與 source map 的保存費用就停了。

安裝

請用 0.1.2 以上。它從箱子任何一個網址的 host 讀箱子 id:<id>-<key>.box.parallelsandbox.com(sceneUrl,第一個宣告的服務是 web 時也是它的 url)與 <id>-<key>-<targetPort>.box.parallelsandbox.com(之後的 web 服務的 url),而且不送 key:訊息、stack、route 與 extra 裡的每個 <id>-<key> 在送出頁面之前都換成 <id>,所以 https://<id>-<key>-5173.box.parallelsandbox.com/app.js 記成 https://<id>-5173.box.parallelsandbox.com/app.js。log 服務收到時也會再換一次。更早的版本不會從 web 服務的網址讀箱子 id。

用 bundler 的話從 npm 裝:

npm install @parallelsandbox/log@^0.1.2

不用 bundler 的話用 import map(@0.1 是 0.1.x 的最新版):

<script type="importmap">
{ "imports": { "@parallelsandbox/log": "https://cdn.jsdelivr.net/npm/@parallelsandbox/log@0.1/+esm" } }
</script>

初始化

import { init } from '@parallelsandbox/log';

const log = init({
  writeKey: 'psw_...',   // project 的 write key;它本身就指明是哪個 project
  release: 'a1b2c3d',    // build id;要跟上傳 source map 時用的 release 一樣
});

呼叫一次,越早越好。writeKey 不是 psw_ 開頭時 init 會丟錯;再呼叫一次會換掉前一個 logger。選項:

選項 預設 說明
writeKey 必填 project 的 write key
endpoint https://log.parallelsandbox.com log 服務
release 空 build id;跟上傳的 source map 對得上,stack 才會還原
boxId 從頁面 host 讀 箱子 id;從箱子網址的 host(<id>-<key>.box.parallelsandbox.com 或 <id>-<key>-<targetPort>.box.parallelsandbox.com)讀,其他地方是空的
boxDomain box.parallelsandbox.com 箱子 host 的網域
captureConsole true 掛上 console.debug、log、info、warn、error
captureErrors true 收 window 的錯誤與未處理的 promise rejection
levels ['info', 'warn', 'error'] 要送的等級;要送 console.debug 就加上 'debug'
flushIntervalMs 2000 多久送一次
batchSize 50 每個請求幾筆,最多 200
maxBuffer 1000 記憶體裡最多留幾筆,超過的新 log 會丟掉
platform 'web' 每批都帶的標籤

init 之後:

  • console.log 以 info 送,其他 console 方法用各自的等級;未處理的錯誤會變成 [uncaught] <訊息> at <檔案>:<行>:<欄> 並附 stack,未處理的 rejection 變成 [unhandledrejection] <訊息>。
  • 每隔 flushIntervalMs 批次送出,頁面隱藏或卸載時把剩下的送掉。
  • 每批都帶 release、platform、user agent、頁面的路徑與 query(route),以及箱子 id。頁面經箱子的網址供應時,箱子 id 從它的 host 取,key 不會離開頁面:這批 log 裡的每個 <id>-<key> 都換成 <id>;log 服務收到時也會再換一次,並從頁面的 origin 取箱子 id。在箱子裡用 localhost 打開的頁面沒有箱子 id,除非你給 boxId。

手動呼叫

init 回傳 logger:

log.info('checkout started', { cart: 3 });
log.warn('slow response', { ms: 2400 });

try {
  riskyThing();
} catch (err) {
  log.error(err, { step: 'riskyThing' }); // Error 會保留 stack
}

await log.flush(); // 測試裡要離開頁面之前

debug、info、warn 接一個訊息與選填的 extra 物件;error 接一個 Error 或任何值,加選填的 extra。extra 是任何能轉成 JSON 的物件,可以用 logs_search 搜。logger 另外有 setBox(id)、setRelease(release)、boxId 與 uninstall()(把 console 與 window 的事件還原)。

Source map

壓縮過的 stack 對 agent 沒有用。每次 build 都用你給 init 的同一個 release 上傳 source map,logs_errors 回的 stack 就會還原到原始檔案與行號。

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"

每個 map 是一個檔案欄位,欄位名稱就是 map 在網站上的路徑,相對於頁面的 origin(stack 裡那支 script 再加 .map);一個請求可以帶同一個 release 的所有 map。上傳用帳號的 token,不是 write key。GET /v1/projects/{id}/sourcemaps?release= 列出傳過的。example-web-with-logs 有一支腳本,整個 dist/ 目錄一次傳完。

stack 只在錯誤送到的那一刻還原一次,而且只還原有 stack、有 release 的 error 等級。先傳 source map 再讓頁面跑;在那之前送到的錯誤會維持壓縮過的 stack。

從 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>" }

每一列有 ts、ingestedAt、level、box、release、platform、route、msg、stack、stackResolved、extra 與 stream。logs_search 與 logs_errors 由舊到新回,附 nextCursor;logs_tail 是長輪詢,有新的列或等到 waitSec 就回,附給下一次呼叫用的 cursor。參數見工具參考。

費用

log 的寫入與保存跟其他東西一樣計量:

用量 點數
寫入 每 GB 760 點
保存 每 GB 每月 33 點

一個每天回報幾百個錯誤的網頁,一個月遠低於一點。