Cloudflare Workers Browser Rendering API を使ってスクリーンショットを R2 に保存する

# Workers Browser Rendering API とは

> [Automate an isolated browser instance with just a few lines of code](https://blog.cloudflare.com/introducing-workers-browser-rendering-api/)
>
> This is why we’re excited to announce a private beta of the Workers Browser Rendering API, improving the browser automation experience for developers. With browser automation, you can programmatically do anything that a user can do when interacting with a browser.
>
> ![Rendering API architecture diagram](https://blog.cloudflare.com/content/images/2022/11/image2-32.png)

2022年12月7日時点で Private beta ですが、その様子をお届けできればと思います。

また、「[WorkersブラウザレンダリングAPI | Cloudflare](https://www.cloudflare.com/ja-jp/lp/workers-browser-rendering-api/)」から waitlist に登録可能なので、ぜひ。

# 環境準備

ディレクトリを準備して、`wrangler init` します。TypeScript を使います。

> Choose “y” to use TypeScript

```shell
mkdir browser-worker && cd $_
npm i @cloudflare/puppeteer
wrangler init
```

以下のパッケージをインストールすることが必要です。

> [@cloudflare/puppeteer - npm](https://www.npmjs.com/package/@cloudflare/puppeteer)

```
npm i @cloudflare/puppeteer
```

スクリーンショット保存用の R2 バケット `browser-worker` を作成します。

```
wrangler r2 bucket create browser-worker
```

`wrangler.toml` に `node_compat = true` と R2 バケットのバインディング設定を追加します。

```
cat << EOS >> wrangler.toml
node_compat = true

[[r2_buckets]]
 bucket_name = "browser-worker"
 binding = "BROWSER_BUCKET"
EOS
```

# Workers コード

- [kyouheicf/browser-worker](https://github.com/kyouheicf/browser-worker)

上記 GitHub のコードを使います。

中身は `https://www.yahoo.co.jp/` ページのスクリーンショットを R2 バケットに保存する内容です。

```javascript:src/index.ts
import puppeteer from '@cloudflare/puppeteer'

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const browser = await puppeteer.launch();
		const page = await browser.newPage();

		await page.setViewport({
			width: 1920,
			height: 1080,
			deviceScaleFactor: 1,
		});

		await page.goto("https://www.yahoo.co.jp/", {
			//一定時間ネットワーク通信のないことで完了を判定する
			waitUntil: "networkidle2", //コネクション数が2個以下である状態が500ミリ秒続いたとき
			timeout: 0 //0を指定するとタイムアウト無し
		});

		const screenshot = await page.screenshot() as Buffer;
		await browser.close();

		const jstNow = new Date(Date.now() + ((new Date().getTimezoneOffset() + (9 * 60)) * 60 * 1000));
		const yyyymmddhhmm = jstNow.toISOString().replace(/[^0-9]/g, '').slice(0, -5); //yyyymmddhhmm
		console.log(yyyymmddhhmm)

		try {
			//upload to R2
			await env.BROWSER_BUCKET.put(`screenshot-${yyyymmddhhmm}.png`, screenshot);
			return new Response(`Success!`);
			/*
			return new Response(screenshot.buffer, {
				headers: {
					'content-type': 'image/png'
				}
			})
			*/
		} catch (e) {
			return new Response('', { status: 400 })
		}
	}
}
```

# デプロイ

以下のコマンドでデプロイします。

```
wrangler publish src/index.ts
```

# 動作確認

## Workers

デプロイした Workers の URL にリクエストを送ると動作が確認できます。

```shell
% curl https://browser-worker.example.workers.dev                              
Success!
# 2022年12月7日時点ではレスポンスが返ってくるまでに 1 分弱の時間がかかっていました
# % curl https://browser-worker.example.workers.dev -s -o /dev/null -w  "%{time_total}\n"        
# 57.886562
```

Workers のリアルタイムログについて、以下のようになります。

```
{
  "outcome": "ok",
  "scriptName": "browser-worker",
  "exceptions": [],
  "logs": [
    {
      "message": [
        "closing websocket"
      ],
      "level": "log",
      "timestamp": 1670386355629
    },
    {
      "message": [
        "202212071312"
      ],
      "level": "log",
      "timestamp": 1670386355629
    },
    {
      "message": [
        "websocket closed"
      ],
      "level": "log",
      "timestamp": 1670386355680
    }
  ],
  "eventTimestamp": 1670386292182,
  "event": {
    "request": {
      "url": "https://browser-worker.example.workers.dev/",
      "method": "GET",
      "headers": {
        "accept": "*/*",
        "accept-encoding": "gzip",
        "cf-connecting-ip": "2a09:bac1:3b60:10::16:18a",
        "cf-ipcountry": "JP",
        "cf-ray": "775a6af618fef625",
        "cf-visitor": "{\"scheme\":\"https\"}",
        "connection": "Keep-Alive",
        "host": "browser-worker.example.workers.dev",
        "user-agent": "curl/7.82.0-DEV",
        "x-forwarded-proto": "https",
        "x-real-ip": "2a09:bac1:3b60:10::16:18a"
      },
      "cf": {
        "longitude": "139.68990",
        "latitude": "35.68930",
        "tlsCipher": "ECDHE-RSA-AES128-GCM-SHA256",
        "continent": "AS",
        "asn": 13335,
        "country": "JP",
        "tlsClientAuth": {
          "certIssuerDNLegacy": "",
          "certIssuerSKI": "",
          "certSubjectDNRFC2253": "",
          "certSubjectDNLegacy": "",
          "certFingerprintSHA256": "",
          "certNotBefore": "",
          "certSKI": "",
          "certSerial": "",
          "certIssuerDN": "",
          "certVerified": "NONE",
          "certNotAfter": "",
          "certSubjectDN": "",
          "certPresented": "0",
          "certRevoked": "0",
          "certIssuerSerial": "",
          "certIssuerDNRFC2253": "",
          "certFingerprintSHA1": ""
        },
        "tlsExportedAuthenticator": {
          "clientFinished": "e82eec20338be4132fbd52fc177d1b4e50fa3d852a78af97a1447c12295d4ca0",
          "clientHandshake": "30df6cfc6441c53105e05505c92280fba9ea3896a280c30047fd55215437b4f6",
          "serverHandshake": "fa7de861d2db076438bd812505abbf1338d699a7cff2f409131b6a1143b671cd",
          "serverFinished": "1304cc66099f0e019e41701437eae1657b5079821bc4e01c409b4b0d530f26ee"
        },
        "tlsVersion": "TLSv1.2",
        "colo": "NRT",
        "timezone": "Asia/Tokyo",
        "city": "Tokyo",
        "edgeRequestKeepAliveStatus": 1,
        "requestPriority": "weight=16;exclusive=0;group=0;group-weight=0",
        "httpProtocol": "HTTP/2",
        "region": "Tokyo",
        "regionCode": "13",
        "asOrganization": "Cloudflare",
        "postalCode": "xxx-xxxx"
      }
    },
    "response": {
      "status": 200
    }
  },
  "id": 0
}
```

## R2 

R2 にスクリーンショットが保存されたことが確認できます。

![image-20221207115949169](https://cdn.jsdelivr.net/gh/kyouheicf/image@master/uPic/image-20221207115949169.png)

このような画像が保存されたことが確認できました。

![screenshot-202212071138](https://cdn.jsdelivr.net/gh/kyouheicf/image@master/uPic/screenshot-202212071138.png)

# 感想

以下のようにシンプルな Browser Rendering API を使う場合でも、〜300ms と長めの CPU Time が消費されたことがわかります。

![image-20221207132256995](https://cdn.jsdelivr.net/gh/kyouheicf/image@master/uPic/image-20221207132256995.png)

こちらのプランごとの制限に照らし合わせてみても、今回のユースケースでは「Unbound Usage Model」が必要になることがわかります。

（実際に Unbound な Workers を使って実装しました。）

> [Worker limits · Cloudflare Workers docs](https://developers.cloudflare.com/workers/platform/limits/#worker-limits)
>
> ![image-20221207132513608](https://cdn.jsdelivr.net/gh/kyouheicf/image@master/uPic/image-20221207132513608.png)

上記のようなポイントを認識しつつも、実際の使用感としては非常に使いやすいと思いました。

これから諸々の自動化が捗りそうです。

また、バインディングについても、これから出てくるようです。

```toml
bindings = [
 { name = "my_browser” type = "browser" }
]
```

# 参考

- [puppeteerを使ったスクレイピング - Qiita](https://qiita.com/unhurried/items/56ea099c895fa437b56e)
- [puppeteerのoptionsのメモ、とくにwaitUntilの値をよく忘れる - LOCAL-C BLOG](https://blog.local-c.com/archives/3275)
- [puppeteer/puppeteer.page.goto.md at v18.2.1 · puppeteer/puppeteer](https://github.com/puppeteer/puppeteer/blob/v18.2.1/docs/api/puppeteer.page.goto.md#remarks)
- [JavaScript で実行環境に左右されず常に JST 日本時間を取得する - Neo's World](https://neos21.net/blog/2020/12/09-01.html)