範例 repo

https://github.com/parallel-sandbox 底下的兩個公開 repo。兩個都能在本機用 docker compose up 跑起來,也是我們測試平台本身用的。下面的工具呼叫是目前在箱子裡跑它們的做法。

example-compose-app

https://github.com/parallel-sandbox/example-compose-app

一個把工作排進佇列的 Node API(port 3000)與一個把工作做完的 Go worker(port 8080),用 docker-compose.yml 接在一起。e2e/ 裡是一個 Playwright 測試:在真的瀏覽器裡打開頁面、加一個工作、等 worker 做完、存一張截圖。

在箱子裡會用到:git clone、docker compose up --build、在箱子裡就解析得到的服務名字、虛擬螢幕上的有頭瀏覽器、sandbox_shot 錄影、用 sandbox_get 拿截圖、用 sandbox_review 把結果交給人。

sandbox_start { "name": "example-compose-app e2e", "goal": "在箱子螢幕上的真瀏覽器跑 example-compose-app 的 e2e 測試;測試通過、截圖與影片都存好就算完成", "services": [{ "name": "api", "port": 3000, "web": true }, { "name": "worker", "port": 8080 }], "orientation": "landscape" }
sandbox_exec  { "id": "<id>", "cmd": "git clone https://github.com/parallel-sandbox/example-compose-app.git", "note": "clone 範例 repo" }
sandbox_exec  { "id": "<id>", "cmd": "docker compose up -d --build --wait", "cwd": "example-compose-app", "timeoutSec": 600, "note": "build 並啟動 api 與 worker" }
sandbox_exec  { "id": "<id>", "cmd": "curl -s http://api:3000/api/health", "note": "確認用名字連得到 api" }
sandbox_exec  { "id": "<id>", "cmd": "npm ci && npx playwright install chromium", "cwd": "example-compose-app/e2e", "timeoutSec": 600, "note": "安裝 e2e 測試需要的套件" }
sandbox_shot  { "id": "<id>", "record": "start" }
sandbox_exec  { "id": "<id>", "cmd": "HEADED=1 BASE_URL=http://localhost:3000 npx playwright test", "cwd": "example-compose-app/e2e", "timeoutSec": 300, "note": "在箱子螢幕上跑 e2e 測試" }
sandbox_shot  { "id": "<id>", "record": "stop" }
sandbox_get   { "id": "<id>", "path": "example-compose-app/e2e/screenshots/jobs.png" }
sandbox_review { "id": "<id>", "what": "輸入工作標題、按 Add job:幾秒內狀態會從 pending 變成 done,並出現 sha256 結果。我在箱子螢幕上跑過 e2e 測試,錄影在「截圖與錄影」。", "open": "web", "port": 3000 }

這個流程以 sandbox_review 收尾,不是 sandbox_stop:箱子會在人 app 的「待驗收」等著,人在它的卡片上打開頁面,也在「截圖與錄影」看得到那段錄影和每一步的截圖。箱子停掉之後,app 裡這些都看不到了(收尾)。

orientation: "landscape" 讓箱子的螢幕是 1280 × 800,測試打開的桌面版頁面放得下;不帶的話螢幕是 390 × 844 直向(sandbox_start)。

api 與 worker 從箱子一開起來就在箱子裡各自解析到自己的位址,api:3000 與 worker:8080 連到發布出來的 port,兩個容器之間也透過 compose 網路互相找得到,所以不需要接線。

另一種跑法:你改了 api,而 staging 上已經有一個 worker 在 https://staging.example.com 跑著。給 externalBaseUrl;兩個服務一開始都是 external:api:3000 與 worker:8080 的 HTTP 都轉到那裡。把 api 切成 box,而且只啟動 api(boxd 不佔它的 port,先後都可以);worker 照樣連到 staging,路徑保留:

sandbox_start { "name": "改過的 api 接 staging 的 worker", "goal": "用 staging 的 worker 測改過的 api;經改過的 api 加進去的工作被 staging 的 worker 做完就算完成", "services": [{ "name": "api", "port": 3000 }, { "name": "worker", "port": 8080 }], "externalBaseUrl": "https://staging.example.com" }
sandbox_exec  { "id": "<id>", "cmd": "git clone https://github.com/parallel-sandbox/example-compose-app.git", "note": "clone 範例 repo" }
sandbox_wire  { "id": "<id>", "service": "api", "mode": "box" }
sandbox_exec  { "id": "<id>", "cmd": "docker compose up -d --build --wait api", "cwd": "example-compose-app", "timeoutSec": 600, "note": "只 build 並啟動 api" }

example-web-with-logs

https://github.com/parallel-sandbox/example-web-with-logs

一個掛了瀏覽器 log SDK @parallelsandbox/log 的靜態頁面。一個按鈕會在三層函式呼叫深處丟出未處理的錯誤。頁面用 esbuild 打包(壓縮、附 source map),由 nginx 提供,容器啟動時才從環境變數寫入設定,所以 project 的 write key 不會進 image。

在箱子裡會用到:sandbox_start 的 secrets、上傳 source map、虛擬螢幕上的頁面、logs_errors 回傳的錯誤 stack 還原到 src/app.js 的行號。log project 必須允許打開頁面的那個 origin;這裡是箱子裡的 http://localhost:8080(見 Log SDK)。

sandbox_start { "name": "example-web-with-logs", "goal": "確認頁面上丟出的錯誤會進到 log;logs_errors 回得到它、stack 還原到 src/app.js 就算完成", "services": [{ "name": "web", "port": 8080, "web": true }], "secrets": ["PSBX_LOG_WRITE_KEY"], "orientation": "landscape" }
sandbox_exec  { "id": "<id>", "cmd": "git clone https://github.com/parallel-sandbox/example-web-with-logs.git", "note": "clone 範例 repo" }
sandbox_exec  { "id": "<id>", "cmd": "PSBX_RELEASE=$(git rev-parse --short HEAD) PSBX_LOG_PROJECT=<project id> PSBX_LOG_ENDPOINT=https://log.parallelsandbox.com docker compose up -d --build --wait", "cwd": "example-web-with-logs", "timeoutSec": 600, "note": "build 並帶 log 設定提供頁面" }

source map 從你自己的機器上傳,不要從箱子,這樣你的 API key 不會進箱子。同一個 commit,所以 release id 一樣:

git clone https://github.com/parallel-sandbox/example-web-with-logs.git && cd example-web-with-logs
npm ci && PSBX_RELEASE=$(git rev-parse --short HEAD) npm run build
PSBX_API_KEY=<api key> PSBX_LOG_PROJECT=<project id> ./scripts/upload-sourcemaps.sh

stack 是在錯誤送到時還原的,所以先傳 source map 再按按鈕。回到箱子:

sandbox_exec  { "id": "<id>", "cmd": "chromium --no-sandbox --kiosk --window-size=1280,800 --user-data-dir=/tmp/chrome http://localhost:8080/", "background": true, "note": "在箱子螢幕上打開頁面" }
sandbox_shot  { "id": "<id>" }
sandbox_exec  { "id": "<id>", "cmd": "xdotool mousemove 640 300 click 1", "note": "按下會丟錯誤的按鈕" }
logs_errors   { "project": "<project id>", "since": "10m" }
sandbox_stop  { "id": "<id>" }

上面的 Chromium 參數與點擊位置 640 300 都是照這個箱子開的橫向螢幕寫的。直向的箱子(390 × 844,預設)要把 --window-size=1280,800 換成 --force-device-scale-factor=0.78 --window-size=500,1082 --window-position=0,0 開 Chromium,按鈕位置照截圖取(sandbox_exec)。

在 localhost 打開的頁面不帶箱子 id(SDK 只從箱子網址的 host 讀),所以這裡用時間篩,不用 boxId。

拿來當範本

兩個 repo 都很單純:根目錄一個 docker-compose.yml,每個服務一個目錄、有自己的 Dockerfile,測試放自己的目錄。長這樣的 repo 在箱子裡都用同一套方式跑。唯一跟 ParallelSandbox 有關的是 sandbox_start 的 goal 與 services 清單(箱子要做什麼;名稱與 port),以及想看瀏覽器錯誤時的 log SDK。