@ah-monica/cloudflare 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,20 @@
1
1
  # @ah-monica/cloudflare
2
2
 
3
- Cloudflare WorkersからMONICAへ、捕捉した例外をbest effortで送るadapter。
4
- Node.js APIへ依存せず、Module Workerの`ExecutionContext.waitUntil()`へ渡せる
5
- Promiseを提供する。
3
+ Cloudflare Workers から MONICA へ、捕捉した例外を送る adapter。
4
+ Node.js API に依存しないので `nodejs_compat` フラグは要らない。
5
+
6
+ 共通の使い方・オプション・制約は [ルートの README](../README.md) にある。
7
+
8
+ ## インストール
9
+
10
+ ```bash
11
+ npm install @ah-monica/cloudflare
12
+ ```
13
+
14
+ ## 初期化
15
+
16
+ DSN には secret key(`msk_...`)を使い、Cloudflare の secret に保存する(`vars` には置かない)。
17
+ staging と production では project・鍵・`environment` を分ける。
6
18
 
7
19
  ```ts
8
20
  import { createCloudflareClient } from "@ah-monica/cloudflare";
@@ -13,26 +25,72 @@ interface Env {
13
25
  MONICA_RELEASE?: string;
14
26
  }
15
27
 
28
+ function createClient(env: Env) {
29
+ return createCloudflareClient({
30
+ dsn: env.MONICA_DSN,
31
+ environment: env.MONICA_ENVIRONMENT,
32
+ release: env.MONICA_RELEASE,
33
+ beforeSend(item) {
34
+ // どの値が個人情報かはアプリケーション固有。送ってよい値だけを残す。
35
+ delete item.user;
36
+ if (item.request) delete item.request.headers;
37
+ return item;
38
+ },
39
+ });
40
+ }
41
+ ```
42
+
43
+ `MONICA_RELEASE` には deploy する git の commit sha を入れる(例: `wrangler deploy --var MONICA_RELEASE:$(git rev-parse HEAD)`)。
44
+ ブラウザ側の SDK にも同じ値を渡すと、Worker とブラウザの event が同じ release に揃う。
45
+
46
+ DSN を Secrets Store に置く場合、binding の値は `get()` で非同期に読むので、client は
47
+ request の入口で作る([制約](#制約) のとおり request ごとに作ってよい)。`get()` は secret が
48
+ 無いと例外を投げるので、`undefined` に落として何も送らない client にする。
49
+
50
+ ```ts
51
+ interface Env {
52
+ MONICA_DSN: SecretsStoreSecret;
53
+ MONICA_ENVIRONMENT: string;
54
+ MONICA_RELEASE?: string;
55
+ }
56
+
16
57
  export default {
17
58
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
59
+ const monica = createCloudflareClient({
60
+ dsn: await env.MONICA_DSN.get().catch(() => undefined),
61
+ environment: env.MONICA_ENVIRONMENT,
62
+ release: env.MONICA_RELEASE,
63
+ });
64
+ ctx.waitUntil(monica.flush());
18
65
  try {
19
66
  return await handleRequest(request, env);
20
67
  } catch (error) {
21
- const monica = createCloudflareClient({
22
- dsn: env.MONICA_DSN,
23
- environment: env.MONICA_ENVIRONMENT,
24
- release: env.MONICA_RELEASE,
25
- beforeSend(item) {
26
- // PIIの定義はアプリケーション固有。送信可能な値だけを残す。
27
- delete item.user;
28
- if (item.request) delete item.request.headers;
29
- return item;
30
- },
31
- });
68
+ monica.captureExceptionInBackground(ctx, error);
69
+ return new Response("Internal Server Error", { status: 500 });
70
+ }
71
+ },
72
+ };
73
+ ```
74
+
75
+ ## 使い方
32
76
 
77
+ request の入口で client を作り、`ctx.waitUntil(monica.flush())` を呼ぶ([稼働確認](#稼働確認))。
78
+
79
+ `captureException()` は capture と flush の両方を終えてから解決する Promise を返す。
80
+ handler の応答を待たせたくない場合は `captureExceptionInBackground(ctx, ...)` で
81
+ `ExecutionContext.waitUntil()` に載せる。
82
+
83
+ ```ts
84
+ export default {
85
+ async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
86
+ const monica = createClient(env);
87
+ ctx.waitUntil(monica.flush());
88
+ try {
89
+ return await handleRequest(request, env);
90
+ } catch (error) {
33
91
  monica.captureExceptionInBackground(ctx, error, {
34
92
  tags: { operation: "handle-request" },
35
- // URL pathにもPIIが入り得るため、利用側で安全と判断した値だけを渡す。
93
+ // URL の path にも個人情報が入り得るので、安全と判断した値だけを渡す。
36
94
  request: { method: request.method, url: new URL(request.url).origin },
37
95
  });
38
96
  return new Response("Internal Server Error", { status: 500 });
@@ -41,53 +99,60 @@ export default {
41
99
  };
42
100
  ```
43
101
 
44
- `captureException()`はcaptureとflushを完了するPromiseを返す。レスポンスを待たせたく
45
- ないHTTP handlerでは`captureExceptionInBackground(ctx, ...)`を使う。CronやQueueの
46
- handlerで送信完了を処理結果に含めたい場合は、直接`await`してよい。
102
+ Cron や Queue の handler のように送信完了を処理結果に含めたい場合は、直接 `await` する。
47
103
 
48
104
  ```ts
105
+ const monica = createClient(env);
49
106
  await monica.captureException(error, { tags: { trigger: "scheduled" } });
50
107
  ```
51
108
 
52
- ## 送信が拒否されたとき
109
+ `captureMessage()` も同じく capture と flush を終えてから解決するので、`flush()` を
110
+ 別途呼ぶ必要は無い。scope(`setUser` / `addBreadcrumb` / `withScope`)は持たず、
111
+ 文脈は capture の第 2 引数で渡す。
53
112
 
54
- `422`(envelope schema 不正)のとき、既定で`console.warn`へ1行出す(Workersのログに出る)。
113
+ Worker の外側で起きた未捕捉例外まで集めたい場合は Tail Worker も検討する。この adapter は、
114
+ アプリケーションが捕捉して業務上の文脈を選んで送る例外を対象にする。
55
115
 
56
- ```
57
- monica: ingest rejected the envelope with 422 (invalid_envelope): 1 issue(s); $.items[0].request.method: Invalid type: Expected string
58
- ```
116
+ ## 稼働確認
59
117
 
60
- 出力先を変える場合は`onDiagnostic`を渡す。`null`を渡すと何も出さない。
61
- `flush()`の戻り値の`status` / `issues` / `error`からも取得できる。
118
+ 共通の仕組みは [ルートの README](../README.md#稼働確認) にある。
62
119
 
63
- `401`(鍵の失効・種別違い)を受けると、そのclientからは以後1回もPOSTしない。
64
- `flush()`の戻り値の`stopped: true`と次の1行で分かる。
120
+ - isolate で最初の `flush()` / `captureException()` / `captureMessage()` の呼び出しが
121
+ `trigger: "start"` を 1 通送る。client を作っただけでは送らない。`dsn` と `environment` の組ごとに数える
122
+ - request の入口で `ctx.waitUntil(monica.flush())` を呼ぶ。`waitUntil` が無いと handler の終了で送信が打ち切られる
123
+ - Workers にはタイマーが無いので `trigger: "interval"` は送らない。isolate が作り直されるたびに `start` を送る
124
+ - 状態は isolate のメモリに持ち、ストレージには書かない
65
125
 
66
- ```
67
- monica: ingest rejected the envelope with 401 (invalid_key); no further envelopes will be sent
68
- ```
126
+ ## オプション
69
127
 
70
- `413`(body上限超過)を受けると`items`を半分に割って送り直す。SDKは契約(gzip後
71
- 1 MiB)を下回るサイズしか送らないので、返るのは経路上のproxyやgatewayが契約より
72
- 低い上限を持っているとき。分割して受理されても`flush()`の戻り値の`status`は`413`に
73
- なり、次の1行をtransportにつき1回だけ出す。
128
+ | option | 型 | default | 説明 |
129
+ | --- | --- | --- | --- |
130
+ | `dsn` | `string \| null` | なし | `https://msk_...@<ingest-host>`。未指定・空文字なら何も送らない |
131
+ | `environment` | `string` | 必須 | 1〜128 文字 |
132
+ | `release` | `string` | なし | item の `release` に載る |
133
+ | `sampleRate` | `number` | `1` | 0〜1 |
134
+ | `requestTimeoutMs` | `number` | `2000` | 1 回の HTTP request の上限 |
135
+ | `flushTimeoutMs` | `number` | `2000` | `captureException` / `captureMessage` が内部で待つ flush の上限 |
136
+ | `maxRetries` | `number` | `0` | `429` / `5xx` / network 障害の再送回数 |
137
+ | `maxCauseDepth` | `number` | `10` | 辿る `cause` の段数 |
138
+ | `maxStackFrames` | `number` | `200` | 送る stack frame 数 |
139
+ | `onDiagnostic` | `(diagnostic) => void \| null` | `console.warn` に 1 行 | 拒否されたときの診断の受け取り先。`null` で無効 |
140
+ | `beforeSend` | `(item, hint) => item \| null \| Promise<...>` | なし | `null` を返すと破棄 |
141
+ | `fetch` | `typeof fetch` | `globalThis.fetch` | 送信に使う fetch |
74
142
 
75
- ```
76
- monica: ingest rejected the envelope with 413 (unknown); splitting and resending. A size limit on the path may be below the 1 MiB (gzip) contract
77
- ```
143
+ ## 送信結果と診断
78
144
 
79
- ## PII方針
145
+ 拒否されたときは既定で `console.warn` に 1 行出る(Workers のログに出る。`422` / `401` / `413`)。
146
+ `flush()` の戻り値の `status` / `issues` / `error` / `stopped` からも取れる。
147
+ 詳しくは [TROUBLESHOOTING.md](../TROUBLESHOOTING.md)。
80
148
 
81
- SDKは、例外message、stack、request、contextがPIIかどうかを推測せず、自動除去も
82
- しない。送信値の選択と`beforeSend`での除去は利用アプリケーションの責任。
83
- MONICAサーバの既知credentialフィルターは防御層であり、任意のPIIが除去される保証
84
- ではない。
149
+ ## 制約
85
150
 
86
- ## API keyと環境
151
+ - `captureExceptionInBackground` を使わず、`waitUntil()` にも載せずに handler を返すと、
152
+ 送信は途中で打ち切られる。
153
+ - client は request ごとに作ってよい。`401` で止まるのはその client だけなので、
154
+ 鍵が失効しても次の request で再び 1 回 POST する。
87
155
 
88
- `dsn`にはCloudflare secretへ保存したsecret project key(`msk_...`)を使う。
89
- stagingとproductionではproject、key、`environment`を分離し、package versionは共通で
90
- よい。secretを`vars`、ソースコード、ログへ置かない。
156
+ ## ライセンス
91
157
 
92
- 未捕捉例外をWorkerの外側から網羅的に収集する用途にはTail Workerも検討する。この
93
- adapterは、アプリケーションが捕捉し、業務contextを選んで送る例外を対象にする。
158
+ [Apache-2.0](LICENSE)
package/dist/client.js CHANGED
@@ -3,6 +3,10 @@ import { SDK_VERSION } from "./version.js";
3
3
  const DEFAULT_FLUSH_TIMEOUT_MS = 2_000;
4
4
  const DEFAULT_MAX_CAUSE_DEPTH = 10;
5
5
  const DEFAULT_MAX_STACK_FRAMES = 200;
6
+ // client は request ごとに作られるので、start を送ったかは isolate に 1 つ持つ。
7
+ // 判定の前に印を付けるので、並行する request でも isolate ごとに 1 回になる
8
+ // (dsn と environment が違えば別に数える)
9
+ const startedInIsolate = new Set();
6
10
  export function createCloudflareClient(options) {
7
11
  assertPositiveInteger("flushTimeoutMs", options.flushTimeoutMs);
8
12
  assertPositiveInteger("maxCauseDepth", options.maxCauseDepth);
@@ -10,6 +14,7 @@ export function createCloudflareClient(options) {
10
14
  const flushTimeoutMs = options.flushTimeoutMs ?? DEFAULT_FLUSH_TIMEOUT_MS;
11
15
  const maxCauseDepth = options.maxCauseDepth ?? DEFAULT_MAX_CAUSE_DEPTH;
12
16
  const maxStackFrames = options.maxStackFrames ?? DEFAULT_MAX_STACK_FRAMES;
17
+ const presenceKey = `${options.dsn ?? ""}\n${options.environment}`;
13
18
  const core = createCoreClient({
14
19
  transport: createFetchTransport({
15
20
  dsn: options.dsn,
@@ -24,12 +29,28 @@ export function createCloudflareClient(options) {
24
29
  sampleRate: options.sampleRate,
25
30
  beforeSend: options.beforeSend,
26
31
  sdk: { name: "@ah-monica/cloudflare", version: SDK_VERSION },
32
+ presence: { platform: "javascript" },
27
33
  });
34
+ /**
35
+ * Workers にはタイマーが無く、global scope では fetch できない。生成時ではなく最初の
36
+ * flush / capture の呼び出し(request の中)で start を送る。送信中の分は flush() が
37
+ * 待つので、それを waitUntil に載せれば handler の後まで生きる。
38
+ */
39
+ function startOnce() {
40
+ if (startedInIsolate.has(presenceKey))
41
+ return;
42
+ startedInIsolate.add(presenceKey);
43
+ void core.checkPresence("start");
44
+ }
45
+ function flush(timeoutMs) {
46
+ startOnce();
47
+ return core.flush(timeoutMs);
48
+ }
28
49
  async function captureAndFlush(input, hint) {
29
50
  try {
51
+ startOnce();
30
52
  const eventId = await core.capture(input, hint);
31
- if (eventId === null)
32
- return null;
53
+ // 捨てた event でも、送信中の start は待つ
33
54
  await core.flush(flushTimeoutMs);
34
55
  return eventId;
35
56
  }
@@ -75,7 +96,7 @@ export function createCloudflareClient(options) {
75
96
  captureException,
76
97
  captureMessage,
77
98
  captureExceptionInBackground,
78
- flush: core.flush,
99
+ flush,
79
100
  close: core.close,
80
101
  };
81
102
  }
package/dist/types.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { BeforeSend, FetchLike, FlushResult, MonicaBreadcrumb, MonicaLevel, MonicaRequest, MonicaUser, TransportDiagnosticHandler } from "@ah-monica/core";
2
2
  export interface CloudflareClientOptions {
3
- dsn: string;
3
+ /** 未指定・空文字・空白だけなら何も送らない */
4
+ dsn?: string | null;
4
5
  environment: string;
5
6
  release?: string;
6
7
  sampleRate?: number;
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** package.json の version と同じ値。bun run version X.Y.Z が書き換える */
2
- export declare const SDK_VERSION = "0.2.1";
2
+ export declare const SDK_VERSION = "0.4.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** package.json の version と同じ値。bun run version X.Y.Z が書き換える */
2
- export const SDK_VERSION = "0.2.1";
2
+ export const SDK_VERSION = "0.4.0";
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "@ah-monica/cloudflare",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "Cloudflare Workers adapter for sending application errors to MONICA",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "sideEffects": false,
8
8
  "files": ["dist", "README.md"],
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
9
11
  "exports": {
10
12
  ".": {
11
13
  "types": "./dist/index.d.ts",
@@ -30,7 +32,7 @@
30
32
  "directory": "cloudflare"
31
33
  },
32
34
  "dependencies": {
33
- "@ah-monica/core": "0.2.1"
35
+ "@ah-monica/core": "0.4.0"
34
36
  },
35
37
  "devDependencies": {
36
38
  "@types/bun": "^1.2.21",