@sentry/roach 0.2.0 → 0.3.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
@@ -5,16 +5,18 @@ replays them, so a second run of a test suite makes no live calls. The
5
5
  proxy owns every read and write of the recordings. A test runner only gives
6
6
  it rules and tells it when each test starts and ends.
7
7
 
8
- Roach is a shared service (`service.ts`): one deployed proxy for many
8
+ Roach is a shared service (`packages/service`): one deployed proxy for many
9
9
  projects and CI runs. A client points `HTTPS_PROXY` at it and trusts its
10
10
  certificate authority. It installs nothing else. The recordings are in a
11
11
  GCS bucket, which deletes each one 30 days after it was written. See
12
12
  [Service](#service) and [Deploy](#deploy).
13
13
 
14
14
  Roach started in
15
- [`getsentry/junior`](https://github.com/getsentry/junior/tree/main/packages/junior-evals/src/roach).
16
- It uses Node built-ins and the `openssl` command. Only the service uses the
17
- Sentry SDK (`@sentry/node`).
15
+ the evals of
16
+ [`getsentry/junior`](https://github.com/getsentry/junior/tree/main/packages/junior-evals).
17
+ It uses Node built-ins and the `openssl` command. The npm package has no
18
+ dependencies. Only the service uses the Sentry SDK (`@sentry/node`), and the
19
+ service is a private package in `packages/service` that is not published.
18
20
 
19
21
  ## Use with vitest
20
22
 
@@ -30,13 +32,14 @@ export default defineConfig({
30
32
  });
31
33
  ```
32
34
 
33
- The global setup (`src/vitest.ts`) starts one run for the invocation and
35
+ The global setup (`packages/roach/src/vitest.ts`) starts one run for the invocation and
34
36
  gives its proxy variables to the test workers. The setup file
35
- (`src/vitest-setup.ts`) opens one session for each test, so test files
37
+ (`packages/roach/src/vitest-setup.ts`) opens one session for each test, so test files
36
38
  record in parallel workers at the same time. A passed test writes its new
37
39
  recordings. A failed test drops them. In `replay` mode, a test with a miss
38
40
  fails. At the end, the global setup writes the stats of the run to stdout
39
- and to the job summary.
41
+ and to the job summary. After you add Roach, do the check in
42
+ [Find changing values](#find-changing-values).
40
43
 
41
44
  - `ROACH_TOKEN`: the write token. Only the global setup reads it. Without
42
45
  it, the run can only replay, as in jobs of forks.
@@ -48,9 +51,21 @@ and to the job summary.
48
51
  A project that has its own global setup calls `startRoach(project)` from
49
52
  `@sentry/roach/vitest` in it, and returns its teardown.
50
53
 
51
- - It needs Node 24.14 or later (`http.setGlobalProxyFromEnv`).
54
+ - It needs Node 24.14 or later (`http.setGlobalProxyFromEnv`), except in
55
+ `off` mode.
56
+ - Put `@sentry/roach/vitest/setup` before a setup file that starts an HTTP
57
+ mock, such as MSW or nock. The setup file keeps the request functions of
58
+ `node:http` and `node:https` as it loads, so its calls to the service do
59
+ not go to the mock.
52
60
  - The tests of one file share one process, so they run one at a time. Do
53
61
  not use `test.concurrent`.
62
+ - The setup file sets a new global `fetch` dispatcher for each test. A
63
+ dispatcher that you set before, such as one with `compose()` interceptors,
64
+ does not apply in the test. Set it again in a `beforeEach` of a setup
65
+ file that comes after `@sentry/roach/vitest/setup` in `setupFiles`, with
66
+ `sequence.setupFiles: "list"`. Restore it with `onTestFinished`.
67
+ - A client that gives `fetch` its own undici dispatcher, such as
68
+ `@vercel/sandbox`, does not use the proxy. Its requests go direct.
54
69
  - The package has TypeScript sources. Vitest compiles them, but Node does
55
70
  not strip types in `node_modules`, so plain Node scripts cannot import it.
56
71
 
@@ -110,12 +125,15 @@ A repository adds one step and a `roach.json` file (see
110
125
  run: pnpm test
111
126
  ```
112
127
 
113
- The action (`src/action.ts`) starts a run with the token in `auto` mode, or
128
+ The action (`packages/roach/src/action.ts`) starts a run with the token in `auto` mode, or
114
129
  without it in `replay` mode. It runs `run` in bash with the proxy variables
115
- of the run, then ends the run and writes its stats to the job summary.
130
+ of the run, then ends the run and writes its stats to the job summary. The
131
+ `url` input sets the service. Default: `https://roach-proxy.getsentry.net`.
116
132
 
117
133
  - Only `run` uses the proxy. Later steps, such as cache and artifact
118
- uploads, do not. `run` does not get the write token.
134
+ uploads, do not. `run` does not get the write token or the `ACTIONS_*`
135
+ variables of the runner, such as `ACTIONS_RUNTIME_TOKEN` and
136
+ `ACTIONS_ID_TOKEN_REQUEST_TOKEN`. So `run` cannot get an OIDC token.
119
137
  - `run` is one session. When it fails, the run drops its new recordings. A
120
138
  miss in `replay` mode fails the step. The summary names the job, not the
121
139
  test, of each miss. For a session per test, use
@@ -126,16 +144,21 @@ of the run, then ends the run and writes its stats to the job summary.
126
144
  ## Config file
127
145
 
128
146
  The action and the vitest integration read `roach.json` in the working
129
- directory or the closest parent directory (`src/config.ts`):
147
+ directory or the closest parent directory (`packages/roach/src/config.ts`):
130
148
 
131
149
  ```json
132
150
  {
133
- "allow": ["https://api.anthropic.com"],
134
- "secretEnv": ["ANTHROPIC_API_KEY"],
151
+ "allow": ["https://api.anthropic.com", "https://openrouter.ai"],
135
152
  "rules": [
136
153
  {
137
154
  "name": "model",
138
- "match": { "method": "POST", "url": "https://api.anthropic.com/v1/" },
155
+ "match": {
156
+ "method": "POST",
157
+ "url": [
158
+ "https://api.anthropic.com/v1/",
159
+ "https://openrouter.ai/api/v1/"
160
+ ]
161
+ },
139
162
  "values": {
140
163
  "uuid": { "builtin": "uuid" },
141
164
  "build": "build-[0-9]+"
@@ -148,19 +171,23 @@ directory or the closest parent directory (`src/config.ts`):
148
171
  - `allow` and `rules` are those of the run. See [Config](#config).
149
172
  - A value is a regular expression source, or `{ "builtin": "<name>" }`
150
173
  for a pattern of `VALUE_PATTERNS` in `values.ts`.
151
- - `secretEnv`: environment variables that hold credentials. Their values
152
- are the `secrets` of the run.
174
+ - `secretEnv`: environment variables that hold credentials that no request
175
+ header has, such as a token that goes to a model in a prompt. Their
176
+ values are the `secrets` of the run. Do not list API keys that go in a
177
+ header: the proxy redacts those on its own. See
178
+ [Config](#config).
153
179
  - `tenant`: default `GITHUB_REPOSITORY`. Set it, so local runs use the
154
180
  recordings of CI.
155
181
  - `noProxy`: hosts that do not use the proxy. Default: loopback.
156
182
 
157
183
  The `name` of the run is `<GITHUB_RUN_ID>-<GITHUB_RUN_ATTEMPT>` in GitHub
158
- Actions.
184
+ Actions. The run also sends `ci`: `GITHUB_RUN_ID`, `GITHUB_JOB`,
185
+ `GITHUB_REPOSITORY`, and `GITHUB_REF`. Only the spans in Sentry use it.
159
186
 
160
187
  ## Config
161
188
 
162
- A run takes `RunConfig` (`src/types.ts`): `tenant`, `mode`, `rules`,
163
- `allow`, `name`, and `secrets`.
189
+ A run takes `RunConfig` (`packages/roach/src/types.ts`): `tenant`, `mode`, `rules`,
190
+ `allow`, `name`, `secrets`, and `ci`.
164
191
 
165
192
  - `mode`: `auto` replays recordings and records misses. `replay` replays
166
193
  recordings and fails a miss with HTTP 412, so nothing goes live. `record`
@@ -170,21 +197,165 @@ A run takes `RunConfig` (`src/types.ts`): `tenant`, `mode`, `rules`,
170
197
  them. It refuses all other origins with HTTP 403. The upstream host
171
198
  always comes from this list, not from the request. The service sends
172
199
  requests only to public addresses, so an origin that resolves to a
173
- private, loopback, or link-local address also gets HTTP 403.
174
- - `rules`: the traffic to record. `name` uses letters, digits, `_`, `.`,
175
- and `-`. `match` has `method`, `url` (a prefix),
176
- and `headers`. `keyHeaders` adds request headers to the key. `values`
177
- names the values that change from run to run. A run can have 64 rules,
178
- and a rule can have 32 values.
200
+ private, loopback, or link-local address also gets HTTP 403. So does an
201
+ IPv6 address with an IPv4 address in it, such as `[::ffff:808:808]` or
202
+ `[::ffff:0:7f00:1]`. The stats
203
+ of the run count the refused requests by origin, in `refused.notAllowed`
204
+ and `refused.notPublic`. The run report shows them after `Refused:`, so
205
+ look there first when requests fail with HTTP 403. A tunnel to a port
206
+ other than 443 shows as `host:port`. The stats name 32 origins for each
207
+ reason, and count the others as `other origins`.
208
+ - `rules`: the traffic to record. `name` starts with a letter or a digit,
209
+ has at most 64 characters, and uses letters, digits, `_`, `.`, and `-`.
210
+ `match` has `method`, `url`, and `headers`. `url` is a prefix,
211
+ or a list of 1 to 32 prefixes. A URL that starts with one of them
212
+ matches. Use a list when the same API is on many hosts, such as one
213
+ rule with the same `values` for Anthropic, OpenRouter, and OpenAI. The
214
+ key does not use `match`, so a change to `url` keeps the keys. When you
215
+ merge rules into one rule, some keys get a new rule name, so their
216
+ requests record again one time. `keyHeaders` adds request headers to
217
+ the key. `values` names the values that change from run to run. A run
218
+ can have 64 rules, and a rule can have 32 values.
179
219
  - Credentials: the proxy never writes the `secrets` of the service config
180
220
  or of the run. Use `secrets` of the run for the credentials of a CI job
181
221
  that a response can show but no request header has, such as a token that
182
222
  goes to a model in a prompt. The service keeps them only in memory, for
183
- the run. A run can have 32 of them. It also learns the value of each request header whose name can mean a credential,
184
- such as one with `auth`, `token`, `key`, `secret`, `cookie`, or `session`.
223
+ the run. A run can have 32 of them, each of at most 16 Ki characters. It
224
+ also learns the value of each request header and query parameter whose
225
+ name can mean a credential: a name with `auth`, `token`, `secret`,
226
+ `key`, `cookie`, `session`, `password`, `credential`, or `signature`.
185
227
  The match is broad on purpose. It does not learn `x-session-affinity`,
186
228
  because that header has a conversation id and not a credential.
187
- Recordings show `<<redacted>>` in place of each known value.
229
+ Recordings show `<<redacted>>` in place of each known value. The URL of
230
+ a recording, a request sample, and the text of a miss in Sentry also show
231
+ `<<redacted>>` for the value of each such query parameter, at any length
232
+ (see [Misses](#misses)). They also show it for a parameter whose value is
233
+ the value of a credential field of the request body (see below). The
234
+ other parameters do not change.
235
+ A run without the token learns at most 1,024 values, with a total of
236
+ 1 Mi characters. A run with the token learns at most 16,384 values, with
237
+ a total of 16 Mi characters, for credentials that change on each
238
+ request, such as AWS signatures. The whole value and each part of it
239
+ count, also for the `secrets` of the run. The proxy also redacts each
240
+ part with `\/` for `/`, as some servers write it in JSON, and the whole
241
+ value when a `/` is only in parts shorter than 12 characters. These
242
+ forms do not count. A run whose `secrets` go past
243
+ the limit gets HTTP 400 when it starts. Past the limit, a request with a
244
+ new credential gets HTTP 431, and the proxy does not send or record it.
245
+ A request with known credentials goes on. Start a new run to learn more.
246
+ - Compressed bodies: the proxy asks the upstream for a body without a
247
+ content encoding. When the upstream sends `gzip`, `deflate`, `br`, or
248
+ `zstd` all the same, the proxy decodes the body before it redacts and records
249
+ it. It decodes 64 MiB at most. When the body does not decode, or when
250
+ it decodes to more than 64 MiB, the proxy does not record it. It
251
+ writes a log line, and the client gets the body as it came.
252
+ - Bodies that are not text: a recording keeps such a body, such as
253
+ `application/octet-stream`, as base64. The proxy redacts the known
254
+ values in its bytes before it encodes it. Only the bytes of a known
255
+ value change, so a binary body stays binary. The client gets the same
256
+ bytes as a replay gives.
257
+ - Credentials that the upstream makes: in a response body, the proxy also
258
+ redacts the value of each field that holds a credential that a server
259
+ mints, such as the token of an OAuth server. In JSON, only the fields
260
+ of the object at the top count, because a server puts a minted
261
+ credential there. In an `application/x-www-form-urlencoded` body, the
262
+ form fields count. The fields are `access_token`, `refresh_token`, `id_token`,
263
+ `session_token`, `auth_token`, and `client_secret`. A bare `token`
264
+ field counts only when its value starts like a credential: a GitHub
265
+ token (`ghp_`, `gho_`, `ghu_`, `ghs_`, `ghr_`, or `github_pat_`), a JWT
266
+ (`eyJ` and two dots), or a Slack token (`xoxa-`, `xoxb-`, `xoxp-`,
267
+ `xoxr-`, or `xoxs-`). Model APIs use `token` for text, such as in log
268
+ probabilities, so the length alone never counts. The names compare
269
+ without case, `_`, and `-`, so `accessToken` is the same as
270
+ `access_token`.
271
+ - The list is short, and only the top of a JSON body counts, on
272
+ purpose. A model can repeat a part of its response in a later
273
+ request, such as the input of a tool call. A redacted value there
274
+ gives a new key, so the request misses on replay. So the proxy does
275
+ not redact a field such as `password` in a response, or a field at a
276
+ lower depth, or text in a string, such as `?access_token=` in a
277
+ command.
278
+ - A recording shows `<<redacted>>` in place of the value. The proxy
279
+ replaces only the value after the name of its field. All other text
280
+ of the body keeps its bytes, also the same text at other places, and
281
+ the spaces and the order of the fields. So a JSON body stays valid
282
+ JSON. A byte order mark at the start and unicode escapes in the value
283
+ do not stop it. The proxy also does this for a body that is valid
284
+ UTF-8 JSON when its content type is not JSON, or when it has none.
285
+ When a minted value stays in the body after this, the proxy does not
286
+ record the response. It writes a log line, and the client gets the
287
+ body as it came.
288
+ - The proxy does not find these fields in text that is not one JSON
289
+ value, such as the `data:` lines of a stream (`text/event-stream`), or
290
+ JSON that is not valid. It keeps them there. It still redacts each
291
+ known value in such a body. When a field name of the top object comes
292
+ two times, it reads only the last value, as `JSON.parse` does.
293
+ A recording keeps an `application/x-www-form-urlencoded` body as
294
+ text. The `location` header of a recording shows `<<redacted>>` for
295
+ each known value, and for each of these fields in its query and its
296
+ fragment, such as `#access_token=`. Other parameters do not change,
297
+ such as `keyword` or `pageToken`, so a replay can follow the redirect.
298
+ - A live response gives the client the real value, so the run can use
299
+ the credential in its next requests. From then on, the run also
300
+ redacts that value where it knows credentials, when it is 12
301
+ characters or longer. These values do not count toward the limit
302
+ above, because only a run that records gets them, from an upstream
303
+ that `allow` names.
304
+ - A replay gives `<<redacted>>` in place of the value. A client that
305
+ only sends the credential back in a header, such as `Authorization`,
306
+ replays without a change, because the key does not use that header.
307
+ A client that reads the credential, such as one that decodes a JWT
308
+ `id_token`, or that sends it in a body, in the URL query, or in a key
309
+ header, does not work on replay. Do not record such requests with a rule.
310
+ - Credentials in a request body: a request sample and the text of a miss
311
+ in Sentry show `<<redacted>>` for the fields above, and also for
312
+ `bearer_token`, `client_assertion`, `password`, `api_key`,
313
+ `secret_key`, `private_key`, and `secret_access_key`. In JSON, these
314
+ fields count at each depth. In an `application/x-www-form-urlencoded`
315
+ body, the form fields count, also with a nested name such as
316
+ `user[password]`, `password[]`, or `user[password][0]`. No key uses
317
+ this text, so this list can be longer. When the value of such a JSON
318
+ field is an object or an array, each string in it shows `<<redacted>>`,
319
+ at any depth. When the value is a number, or has a number in it, the
320
+ request sample has no text, and the URL of the recording shows
321
+ `<<redacted>>` for a parameter with that number. The proxy replaces the value at the place
322
+ of each field, and each other string that is the same value, also each
323
+ string of an object or an array value. A value pattern can change
324
+ the text of a key, so then the proxy checks the result. It parses the
325
+ body again, and the body must be JSON. Each field must hold
326
+ `<<redacted>>` at its place, and no string can be one of the values.
327
+ Only the body counts, not a key header. When a check fails, the request
328
+ sample has no text, and the proxy writes a log line with the key. This
329
+ occurs when a value pattern takes a quote, or a name of a field, away.
330
+ A request with credential fields also gets no text when the name of a
331
+ value of its rule has `"`, `\`, `&`, `=`, `<`, `>`, or a control
332
+ character, because its `<<name>>` can add a field. A miss has no change
333
+ then.
334
+ - The proxy cannot find the fields of a body that is not a JSON object or
335
+ array or a form, such as JSON with a comma at the end, JSON lines, a
336
+ multipart body, or a JSON string. It also cannot find them in a form body
337
+ that has no credential field, such as JSON or XML with a form content
338
+ type. So the request sample of such a body has no text when the body has
339
+ the name of a field above anywhere, or the start of a bare `token` above.
340
+ The name can be in any case, with `_`, `-`, or nothing between its words,
341
+ such as `accessToken`, and with JSON unicode escapes or URL escapes in
342
+ it. Each test is one search of the body, so a long body costs little.
343
+ In a form with credential fields, the request sample also has no text
344
+ when a field that is not a credential field can show one, such as
345
+ `user.password=`, or a bare `token` or another field with JSON with URL
346
+ escapes in its value. A value that is only such a name, such as
347
+ `grant_type=password`, does not count. It also has no text when a part
348
+ of a nested name, such as `S` of `S[password]`, is a credential value,
349
+ or when a name has the start of a bare `token`.
350
+ - The proxy parses a JSON body to find the fields, so a long string value
351
+ cannot stop it. It does not find a field in JSON text that is itself a
352
+ string, such as the escaped arguments of a tool call, or a copy of a
353
+ value inside other text, such as in a prompt or with URL escapes. The URL
354
+ of a recording keeps a copy of a value of the body when the proxy cannot
355
+ find the fields of the body.
356
+ - Keys: redaction never changes the key of a request. The key is the hash
357
+ of the request as the client sent it, so recordings from before stay
358
+ valid.
188
359
 
189
360
  Requests that match no rule go live without a change, and their responses
190
361
  stream. Each response has an `x-roach` header: `replayed`, `live`,
@@ -193,10 +364,12 @@ stream. Each response has an `x-roach` header: `replayed`, `live`,
193
364
  ## Recordings
194
365
 
195
366
  The key of a recording is `<rule>/<hash>.json`, under `<tenant>/` in the
196
- store. The hash is the hash of the rule, the method, the URL, the key
367
+ store. The hash is the hash of the rule name, the method, the URL, the key
197
368
  headers, and the body. A JSON body has sorted keys. A recording keeps the
198
- response and the session that recorded it. It does not keep the request
199
- body.
369
+ response, the session that recorded it, and the method and the redacted
370
+ URL of the request. Of the response headers, it keeps only `content-type`
371
+ and `location`. It does not keep the request body. A request sample keeps
372
+ it. See [Misses](#misses).
200
373
 
201
374
  - Many sessions can be open at once. A request belongs to the session of
202
375
  its proxy URL (`url` of the session). A request through the proxy URL of
@@ -214,7 +387,7 @@ body.
214
387
  Ids and times change on each run, and a response often repeats them. For
215
388
  example, the model archives the memory with the id that its request
216
389
  showed. `values` of a rule maps a name to a regular expression source.
217
- `VALUE_PATTERNS` in `values.ts` has `uuid`, `isoTime`, `date`, `epochMs`,
390
+ `VALUE_PATTERNS` in `packages/roach/src/values.ts` has `uuid`, `isoTime`, `date`, `epochMs`,
218
391
  `sha256`, and `gitCommit`. When two patterns match at the same place, the first one wins.
219
392
  The proxy matches values in JSON text. Start a custom pattern with
220
393
  `NOT_AFTER_WORD`, not `\b`: after an escape such as `\n`, `\b` sees the `n`
@@ -232,7 +405,12 @@ as part of a word and does not match.
232
405
  the same session, such a value is also a placeholder where no pattern
233
406
  finds it. For example, a pattern finds a short commit id only in the
234
407
  output of `git push`, but the model then quotes it alone in its reply. A
235
- pattern wins at the same place.
408
+ pattern wins at the same place. A session remembers all values of its
409
+ requests until it ends. It does not drop a value, because that would
410
+ change the keys of later requests. Only a value of over 32,767
411
+ characters stays out: V8 cannot run a pattern with it, so it would fail
412
+ each later request of the session. A run without the token has a limit
413
+ (see below): past it, the request fails.
236
414
  - Values in thinking blocks of the request do not count for `n`, and the
237
415
  proxy does not look for remembered values in them. A replay keeps the
238
416
  recorded text of a thinking block, so when a later request sends it back,
@@ -249,22 +427,177 @@ real way use the same recording.
249
427
 
250
428
  The service takes any pattern that compiles in 1 s, up to 2048
251
429
  characters. A run without the token must use the same patterns as the run
252
- that recorded, or its keys do not match. All runs share one process, so
253
- the key of each request has a time limit: 1 s, and 0.5 s for each MiB of
254
- the body. The built-in patterns take less than 0.1 s for each MiB. A
255
- request whose key takes longer fails with HTTP 400, and other runs go on.
430
+ that recorded, or its keys do not match. Each key has a time limit: 1 s,
431
+ and 0.5 s for each MiB of the body. The built-in patterns take about 60
432
+ to 110 ms for each MiB. A request whose key takes longer fails with HTTP 400.
433
+ So does a JSON body with arrays or objects nested over 64 levels deep.
434
+
435
+ All runs share one process. Anyone can start a run without the token, so
436
+ the service makes the keys of these runs in a worker process, one key at
437
+ a time. So a slow key does not stop the other work of the service. Some
438
+ work, such as `JSON.parse()` of a large body, cannot stop at the time
439
+ limit. When a key goes 1 s past its time limit, the service stops the
440
+ worker and starts a new one. The worker has 256 MiB of heap. A key that
441
+ needs more stops the worker, and its request fails with HTTP 400. The key
442
+ of a 16 MiB body of small text blocks needs less. A run with the token
443
+ makes its keys in the main process, so it has no memory limit of the
444
+ worker. The key of a 64 MiB body can need about 1 GiB.
445
+
446
+ The limits never change a key: a request gets the key that it always
447
+ had, or it fails. A run without the token has smaller limits:
448
+
449
+ - A key has at most 5 s.
450
+ - A request can have at most 100,000 changing values. A request with more
451
+ fails with HTTP 400, and the run goes on.
452
+ - The sessions of the run remember at most 10,000 values, with 256 Ki
453
+ characters, together. A request that would add more fails with HTTP
454
+ 400, and the run ends. A session forgets its values when it ends. The
455
+ model requests of Junior have about 500 values with 10 K characters for
456
+ each session.
457
+ - The keys of the run and the redaction of its misses (see
458
+ [Misses](#misses)) have 30 s in total. In the 14 days to 2026-11-05, the
459
+ keys of the largest run took 9.5 s (p99 9.1 s, about 1,000 keys).
460
+ - When a key goes past its time limit, its worker stops, or the run goes
461
+ past 30 s, the run ends. Its other requests then fail with HTTP 410 or 407.
462
+
256
463
  Avoid nested quantifiers, such as `(a+)+`.
257
464
 
258
465
  ## Misses
259
466
 
260
467
  The proxy adds each request without a recording to `stats().misses`, with
261
- its session and key. `describeRecordingMisses()` in `report.ts` gives the
262
- first miss of each test, which is the one to fix.
468
+ its session and key. The stats keep the first 500 misses of a run.
469
+ `describeRecordingMisses()` in `packages/roach/src/report.ts` gives the
470
+ first miss of each test, which is the one to fix. The run report of the
471
+ vitest integration and of the GitHub Action shows these misses.
472
+
473
+ The proxy also finds where the first miss of each session changed
474
+ (`packages/service/src/misses.ts`). Sentry gets the text of the change, on the `roach.request`
475
+ span of the miss (see [Sentry](#sentry)). The report of a run with the
476
+ write token tells only where the change is:
477
+
478
+ ```text
479
+ - a.fixture.ts > passes: model/074d….json
480
+ Changed at character 65 of the request of model/8b96….json. See miss.recorded and miss.request on the roach.request span in Sentry.
481
+ ```
482
+
483
+ The report goes to CI logs and job summaries, and these can be public. So
484
+ no report, log, or stats of a run show the text of a request. Only Sentry
485
+ shows it.
486
+
487
+ - When the proxy records a request, it also writes a request sample to
488
+ `_requests/<rule>/<hash>.json`. The sample has the text that the key
489
+ hashes: the rule, the method, the URL, the key headers, and the body
490
+ with `<<name>>` for each changing value. The proxy redacts credentials
491
+ in it, as in a recording, the value of each credential query parameter,
492
+ and the value of each credential field of the body, such as
493
+ `client_secret` (see [Config](#config)). It keeps the first 1,048,576
494
+ characters (UTF-16 code units) of the text.
495
+ - The hash of a sample is the place of the request: its session (test)
496
+ and the number of requests of the rule that the session sent before
497
+ it. So a sample is the last recorded request at the same place of the
498
+ same test. A replay writes no sample.
499
+ - At the first miss of a session, the proxy reads the sample of the place
500
+ of the request. It finds the first character that is not the same.
501
+ Sentry gets 40 characters before it, and at most 120 characters from it,
502
+ of each text. This is one read for each session, in every run.
503
+ - Sentry gets the change of every run, with or without the token. So find
504
+ the change of a miss of a fork in Sentry.
505
+ - Only runs with the write token get `change` in their stats and report.
506
+ It has the key of the recording that the sample came from (`file`), and
507
+ the offset (`at`). It has no text. A run without the token, such as CI of
508
+ a fork, gets no `change`. Reads are public, and the offset of the first
509
+ change would let anyone guess the requests of a tenant back, one
510
+ character at a time.
511
+ - A miss has no change when there is no sample, such as for a recording
512
+ from an older version, when the sample has no text (`withheld`, see
513
+ [Config](#config)), or when the texts are the same in the part that the
514
+ sample keeps. A sample with no text still takes the place of the older
515
+ sample, so a miss never compares with a request that is not the last.
516
+ - The proxy redacts the text of the miss before it compares it. This has
517
+ the time limit of a key, and for a run without the token it runs in the
518
+ worker process of the keys (see [Changing values](#changing-values)).
519
+ Past the limit, the miss has no change.
520
+ - When the requests of a test run in parallel, their order can change.
521
+ Then a sample can come from another request of the test.
522
+
523
+ ## Find changing values
524
+
525
+ Do this check when you add Roach to a project. Do it again when a test
526
+ that passed does not replay on the next run.
527
+
528
+ A value that changes on each run and that no pattern finds gives a new key
529
+ on each run. In `auto` mode, that request goes live and records again on
530
+ each run. The run is slow, and it costs as much as a run without Roach.
531
+ The requests after it in the test also go live, because the live response
532
+ is not the same as the recorded one. No error shows this. The tests pass.
533
+
534
+ 1. Run the suite two times with the token. On the second run, each test
535
+ that passed must replay all of its requests. Look at `live` in the stats
536
+ of the second run. It must be 0, or near 0. In CI, also compare the
537
+ time of each test. A test that replays takes about as long as its code
538
+ without the network. [`TELEMETRY.md`](TELEMETRY.md) shows how to find
539
+ the runs and keys that stay live.
540
+ 2. Find the first live request of a test that did not replay. Run only
541
+ that test with `ROACH_MODE=replay`. It needs no token. The test fails
542
+ at the first miss, and the run report shows its key. Fix the first miss
543
+ of a test before the others. The others often come from it.
544
+ 3. Find the `roach.request` span of the first miss in Sentry, by its
545
+ `roach.run.id`, `roach.session`, and `has:miss.changed_at`. Read `miss.recorded` and `miss.request`. See
546
+ [Misses](#misses) and [`TELEMETRY.md`](TELEMETRY.md). They show the
547
+ changing value, with the text before it. This works for every run, with
548
+ or without the token. When the miss has no change, get the body of that
549
+ request from two runs, and compare them. For example, add this file to `setupFiles`
550
+ after `@sentry/roach/vitest/setup`, and run the test two times:
551
+
552
+ ```ts
553
+ // Local only. Writes each fetch request to DUMP_FILE.
554
+ import { appendFileSync } from "node:fs";
555
+ import { expect } from "vitest";
556
+
557
+ const dumpFile = process.env.DUMP_FILE ?? "requests.jsonl";
558
+ const fetchBefore = globalThis.fetch;
559
+ globalThis.fetch = async (input, init) => {
560
+ const request = new Request(input, init);
561
+ const record = {
562
+ test: expect.getState().currentTestName,
563
+ method: request.method,
564
+ url: request.url,
565
+ body: await request.clone().text(),
566
+ };
567
+ appendFileSync(dumpFile, JSON.stringify(record) + "\n");
568
+ return fetchBefore(request);
569
+ };
570
+ ```
571
+
572
+ The first difference between the two bodies is the changing value.
573
+ This file sees only `fetch`. A client that uses `node:http` directly
574
+ needs its own hook.
575
+
576
+ 4. Add a narrow pattern for the value to `values` of the rule. See
577
+ [Changing values](#changing-values). Then do step 1 again.
578
+
579
+ Values that changed in real projects:
580
+
581
+ - Ids that the code under test makes, such as UUIDs and random short ids.
582
+ For example, Warden gives each finding a random `XXX-XXX` id, and the
583
+ next model request quotes it.
584
+ - Paths of temporary directories, commit SHAs of a repository that a test
585
+ makes, and times and dates.
586
+ - The local environment. Global git config, such as a hook that adds a
587
+ commit trailer, commit signing, or `user.name`, changes the commits that
588
+ a test makes. `HOME`, the user name, the locale, and the time zone can
589
+ also show in a request. Then local keys are not the same as CI keys, so
590
+ a local `replay` misses while CI replays. Make the test harness ignore
591
+ them. For git, set `GIT_CONFIG_GLOBAL=/dev/null` and
592
+ `GIT_CONFIG_NOSYSTEM=1`, and set the author and committer in the test.
593
+ - Order. The proxy sorts JSON object keys, but not arrays. An array whose
594
+ order depends on timing, such as the results of parallel tool calls,
595
+ gives a new key when the order changes.
263
596
 
264
597
  ## Service
265
598
 
266
- `cli.ts service <config.json>` starts the service. The config is
267
- `RoachServiceConfig` in `src/service.ts`. `deploy/gcp/` writes it for you.
599
+ `packages/service/src/cli.ts service <config.json>` starts the service. The
600
+ config is `RoachServiceConfig` in `packages/service/src/service.ts`. `deploy/gcp/` writes it for you.
268
601
 
269
602
  - **Tenants.** A tenant is a GitHub repository, as `owner/repo`, such as
270
603
  `getsentry/junior`. In GitHub Actions, use `GITHUB_REPOSITORY`. The
@@ -278,10 +611,11 @@ first miss of each test, which is the one to fix.
278
611
  you trust, and never to a workflow that runs code from a fork.
279
612
  - **Reads are public.** Anyone can start a `replay` run of a tenant, so CI
280
613
  jobs of forks replay without a secret. Such a run never writes, and it
281
- refuses a request that no rule records, so nothing goes live. Every
282
- other mode needs the write token. So treat each recording as readable by
283
- anyone who can make its request. Do not record responses that must stay
284
- private.
614
+ refuses a request that no rule records, so nothing goes live. The stats
615
+ count these requests in `refused.replayOnly`. Every other mode needs
616
+ the write token. So treat each recording as readable by anyone who can
617
+ make its request. Do not record responses that must stay private. For
618
+ what a run without the token sees of a miss, see [Misses](#misses).
285
619
  - **Runs.** Each run has its own id, token, mode, rules, sessions, and
286
620
  stats. A proxied request names its run in `Proxy-Authorization`, as
287
621
  `Basic base64(<id>:<token>)`. The proxy URL of the run has them, so
@@ -290,79 +624,153 @@ first miss of each test, which is the one to fix.
290
624
  their value patterns. A tenant that calls a new origin or uses a new
291
625
  pattern changes only its own config. The service needs no change and no
292
626
  restart. Two checks keep one run from harming the others:
293
- - The proxy sends requests only to public addresses (`addresses.ts`).
627
+ - The proxy sends requests only to public addresses
628
+ (`packages/service/src/addresses.ts`).
294
629
  It checks the address of each connection after DNS, so a host name
295
630
  cannot point a run at the metadata server, at the VPC, or at the VM.
296
631
  Only tests and local development set `privateNetworks`.
297
- - The key of a request has a time limit (`request-key.ts`), so a slow
298
- pattern fails only its own request.
632
+ - A key has a time limit. The service makes the keys of runs without
633
+ the token in a worker process with a memory limit
634
+ (`packages/service/src/key-worker.ts`). So a slow pattern or a large body fails only its
635
+ own request, and it does not stop the service.
299
636
  - **Sentry.** With `sentryDsn`, each read and write of a recording sends
300
- a span to Sentry. Each proxied request sends a span and metrics. See
301
- [Sentry](#sentry).
637
+ a span to Sentry. Each proxied request sends a span and metrics. Each
638
+ unexpected error sends an error event. See [Sentry](#sentry).
639
+ - **Errors.** An error that is the fault of the client, such as HTTP 400,
640
+ 409, 413, or 429, keeps its message. Any other error, such as a failure
641
+ of the upstream or of GCS, gets HTTP 502 for a proxied request or HTTP
642
+ 500 for a control call. Its text is only `internal error, Sentry event
643
+ <id>`, because the message of the error can have a response body. Only
644
+ Sentry gets the error. The service log gets its class, its code, and
645
+ the HTTP status of a failed GCS call.
302
646
  - **Certificate authority.** `ca` holds the PEM certificate and key that
303
647
  clients trust. Keep it across restarts. Host certificates live for 7
304
648
  days, and the service makes new ones before they expire.
305
649
 
306
650
  ### Sentry
307
651
 
308
- With `sentryDsn`, the service sends metrics and spans to Sentry. It keeps
309
- all traces. Metrics have only attributes with few values: `tenant`, `mode`,
310
- `rule`, `origin`, and `result`. Spans also have `run` (the `name` of the
311
- run) and `key`.
312
-
313
- | Metric | Type | Value |
314
- | ------------------------ | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
315
- | `roach.request` | counter | 1 for each proxied request. `result` is the `x-roach` header: `replayed`, `live`, `missed`, or `passthrough`. A `passthrough` request has no `rule`. |
316
- | `roach.request.duration` | distribution (ms) | The time to answer a request that a rule records, by `result`. |
317
- | `roach.recording.write` | counter | Recordings by `rule`. `result` is `written` (the proxy sent them to the store) or `dropped` (their session failed). |
318
-
319
- Each proxied request is one trace. Each span has `tenant`, `mode`, and
320
- `run`.
321
-
322
- - `roach.run.start`: a new run, with `write` (the run has the write token).
652
+ With `sentryDsn`, the service sends metrics, spans, and unexpected errors
653
+ to Sentry. It keeps all traces. `TELEMETRY.md` has the queries. Each
654
+ attribute of Roach starts with `roach.`. Metrics have only attributes with
655
+ few values: `roach.tenant`, `roach.mode`, `roach.rule`, `roach.origin`,
656
+ `roach.result`, and `roach.reason`.
657
+
658
+ | Metric | Type | Value |
659
+ | ------------------------ | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
660
+ | `roach.request` | counter | 1 for each proxied request. `roach.result` is `replayed`, `live`, `missed`, `passthrough`, `refused`, `busy` (HTTP 503: the body did not fit in the budget), `timeout` (HTTP 408: the body did not come in time), or `aborted` (the client closed the request before its body came). A `passthrough`, `refused`, `busy`, `timeout`, or `aborted` request has no `roach.rule`. A `refused` request has `roach.reason`, and `other` for an origin not in `allow`. |
661
+ | `roach.request.duration` | distribution (ms) | The time to answer a request that a rule records, by `roach.result`. |
662
+ | `roach.recording.write` | counter | Recordings by `roach.rule`. `roach.result` is `written` (the proxy sent them to the store) or `dropped` (their session failed). |
663
+
664
+ Each proxied request is one trace. Each span has `roach.tenant`,
665
+ `roach.mode`, `roach.run.id` (the `name` of the run, else its id), and the
666
+ `roach.ci.*` attributes of the CI job of the run: `roach.ci.run_id`,
667
+ `roach.ci.job`, `roach.ci.repo`, and `roach.ci.ref`.
668
+
669
+ - `roach.run.start`: a new run, with `roach.write` (the run has the write
670
+ token).
323
671
  - `roach.request`: one proxied request, named `<method> <origin>`, with
324
- `origin`, `rule`, `result`, and `http.response.status_code`. When a
325
- replayed or live response has token usage, the span also has the
326
- `gen_ai` attributes of the model call (`usage.ts`). Sentry then
327
- calculates its cost in USD as `gen_ai.cost.total_tokens`. It contains:
328
- - `roach.key`: the key of the request, with `rule` and `key`.
329
- - `roach.store.read`: a read of a recording, with `key` and `result`:
330
- `hit` or `miss`.
331
- - `roach.upstream`: the live request, with `origin` and
332
- `http.response.status_code`.
333
- - `roach.store.write`: a write of recordings, with `recordings` and
334
- `written`. A session writes when it ends, so this span is usually its
335
- own trace.
336
-
337
- Telemetry never has a credential, a body, or the path or query of a URL.
672
+ `roach.origin`, `roach.rule`, `roach.result`, `roach.session` (the name
673
+ of the session, such as the test), `http.request.method`, and
674
+ `http.response.status_code`. A refused request also has `roach.reason`.
675
+ When a replayed or live response has token usage, the span also has the
676
+ `gen_ai` attributes of the model call
677
+ (`packages/service/src/usage.ts`). Sentry then calculates its cost in USD as
678
+ `gen_ai.cost.total_tokens`. The span of the first miss of a session also
679
+ has `miss.sample` (`found`, `withheld`, or `none`), in every run. When
680
+ the proxy finds a change, the span also has `miss.changed_at` (the
681
+ offset of the first different character), `miss.compared` (the key of
682
+ the recording), and the redacted text of the change: `miss.recorded` and
683
+ `miss.request`.
684
+ Only Sentry gets these texts ([Misses](#misses)). It contains:
685
+ - `roach.key`: the key of the request, with `roach.rule` and
686
+ `roach.key`.
687
+ - `roach.store.read`: a read of a recording, with `roach.key` and
688
+ `roach.result`: `replayed` (the store has the recording) or `missed`.
689
+ The first miss of a session also reads a request sample. Its
690
+ `roach.key` starts with `_requests/`.
691
+ - `roach.upstream`: the live request, with `roach.origin`,
692
+ `http.request.method`, and `http.response.status_code`.
693
+ - `roach.store.write`: a write of recordings, with `roach.recordings` and
694
+ `roach.written`. A session writes when it ends, so this span is usually
695
+ its own trace. A second span writes the request samples of the same
696
+ recordings.
697
+ - `roach.run.end`: the end of a run, with its totals: `roach.replayed`,
698
+ `roach.live`, `roach.missed`, `roach.passthrough`, `roach.refused`,
699
+ `roach.written`, `roach.dropped`, and `roach.run.duration_ms`.
700
+
701
+ An error event has the tags of the spans of its run. An error of the
702
+ control API has no run tags.
703
+
704
+ Telemetry never has a credential. It has no body and no path or query of
705
+ a URL, except `miss.recorded` and `miss.request`. These have at most 160
706
+ characters of the redacted text of a request, which can include a part of
707
+ its URL, headers, or body. The log of the service names only the key of a
708
+ miss, not its change.
338
709
  From a model call, it has only the model name and the token counts.
339
710
  So the service turns off the request data and the HTTP spans of the SDK,
340
- and it adds no trace headers to the requests that it sends.
341
-
342
- To see how well the cache works:
343
-
344
- - **Hit rate.** Sum `roach.request`, group by `result`, and filter by
345
- `tenant` and `rule`. The hit rate is `replayed / (replayed + live +
346
- missed)`.
347
- - **Misses.** `missed` is a request that failed in `replay` mode. `live`
348
- in `auto` mode is a new recording. When `live` stays high from run to
349
- run, a value in the key changes on each run.
350
- - **Time.** Compare `roach.request.duration` for `live` and `replayed`.
351
- - **Dropped.** `roach.recording.write` with `result:dropped` counts
352
- recordings of failed sessions. The next run sends these requests live
353
- again.
354
- - **Changing keys.** Search spans for `span.op:roach.key`, group by
355
- `key`, and count the runs. A key that only one run uses shows that a
356
- value changes on each run.
711
+ removes the path from the name of an error event, and adds no trace
712
+ headers to the requests that it sends. An error event has the message of
713
+ the error, which can have a response body.
714
+
715
+ [`TELEMETRY.md`](TELEMETRY.md) shows how to get the hit rate, the misses,
716
+ the time that replays save, the dropped recordings, and the keys that
717
+ change on each run.
357
718
 
358
719
  ### Limits
359
720
 
360
721
  - Runs live in memory in one process. A restart ends the open runs, and
361
- their CI jobs fail. A run that is open for 6 hours ends as failed.
722
+ their CI jobs fail. A run with the token that is open for 6 hours ends
723
+ as failed.
724
+ - A run without the token ends after 2 hours, or after 60 minutes without
725
+ a proxied request or a control call. Its proxy URL then gets HTTP 407,
726
+ and its control API gets HTTP 404. The GitHub Action starts the run
727
+ before the command of the job, so a step that takes longer than
728
+ 60 minutes without a request to the service fails.
362
729
  - The service can have 100 open runs without a token, for all tenants
363
- together. Another one gets HTTP 429 until one ends. Runs with the token
364
- have no cap.
365
- - A request body over 64 MiB gets HTTP 413.
730
+ together, and 10 for each client address. All addresses of one IPv6
731
+ /64 count as one client, and an IPv4 address in IPv6 form, such as
732
+ `::ffff:192.0.2.1`, counts as its IPv4 address. Another one gets HTTP
733
+ 429 until one ends. Runs with the token have no cap. With `proxyProtocol`, the address of the
734
+ client comes from the PROXY protocol v1 header of the load balancer.
735
+ A connection without the header uses the address of the connection.
736
+ - A proxied request body over 64 MiB gets HTTP 413, and over 16 MiB for a
737
+ run without the token. A control API body over 1 MiB gets HTTP 413
738
+ before the service parses it.
739
+ - The requests of all runs without the token can hold 128 MiB of bodies
740
+ at one time, and the runs of one client 16 MiB. The client of a run is
741
+ the client that started it, grouped as for the limit of runs. A request
742
+ holds its body until it finishes, also while its key waits for the key
743
+ worker. A request whose body does not fit gets HTTP 503 with
744
+ `Retry-After: 1`, and the run goes on. The clients of model APIs send a
745
+ 503 again. A client that does not gets the 503, and its test fails.
746
+ Runs with the token have no such limit.
747
+ - A body of a run without the token that does not come in 30 s gets
748
+ HTTP 408, and its connection closes. The run goes on.
749
+ - With these limits, the service and the key worker use at most about
750
+ 1.1 GB of the 2 GB of the VM. That is for request bodies of runs with
751
+ the token of a normal size. A body of 64 MiB from a run with the token,
752
+ at the same time as runs without the token fill the budget, took about
753
+ 2.0 GB.
754
+ - Keys and the redaction of misses have time limits. See
755
+ [Changing values](#changing-values).
756
+ - Request headers that do not come in 10 s get HTTP 408, and a request
757
+ whose body does not come in 300 s too. This is also true in a tunnel
758
+ and with `proxyProtocol`. With `proxyProtocol`, a connection that sends
759
+ no byte, or no full PROXY header, in 10 s closes.
760
+ - A `CONNECT` tunnel that sends no byte for 10 s after the `200` closes.
761
+ - A connection or tunnel that sends and gets no byte for 10 minutes
762
+ closes, also while a request waits for its response. This is true for
763
+ each request, with or without TLS. Between two requests, a connection
764
+ closes after 6 s without a new request (the `keepAliveTimeout` of Node,
765
+ 5 s, and 1 s that Node adds), and the client opens a new one.
766
+ - The credentials that a run learns and the values that the sessions of
767
+ a run without the token remember have limits. See [Config](#config) and
768
+ [Changing values](#changing-values).
769
+ - A gap: a client with an IPv6 /48 has 65,536 /64s, so it can open runs
770
+ until the service has 100 runs without a token. Then other clients get
771
+ HTTP 429 until its runs end. Its runs can also hold all of the budget of
772
+ bodies. A body gets HTTP 408 after 30 s, but the client can start the
773
+ next one at once, so it holds the budget for as long as it sends.
366
774
  - GCS deletes each recording 30 days after it was written, also when runs
367
775
  still replay it. Then `auto` mode records it again, and `replay` mode
368
776
  fails the request with HTTP 412.
@@ -379,7 +787,11 @@ To see how well the cache works:
379
787
  `ghcr.io/getsentry/roach:main`, with a service account that can only use
380
788
  the bucket and the secret,
381
789
  - an SSL proxy load balancer with a Google-managed certificate. It ends TLS
382
- and passes the raw TCP stream, with `CONNECT`, to the VM,
790
+ and passes the raw TCP stream, with `CONNECT`, to the VM. Each
791
+ connection starts with a PROXY protocol v1 header with the address of
792
+ the client (`proxy_header`). The config sets `proxyProtocol`, so the
793
+ service limits runs without a token by that address. The health check
794
+ sends the header too,
383
795
  - a service account for the `Deploy` workflow, which GitHub Actions uses
384
796
  through workload identity federation (`deploy.tf`). It can only reset
385
797
  the VM.
@@ -394,6 +806,16 @@ both pass and the commit is still the head of `main`, it moves `:main` to
394
806
  the open runs, so their CI jobs fail. A red commit does not deploy, and an
395
807
  older commit never replaces a newer one.
396
808
 
809
+ Sometimes GitHub starts no workflow for a push to `main`. Then the head of
810
+ `main` has no CI and no image, and it does not deploy. Run both by hand:
811
+
812
+ ```sh
813
+ gh workflow run ci.yml --repo getsentry/roach --ref main
814
+ gh workflow run image.yml --repo getsentry/roach --ref main
815
+ ```
816
+
817
+ The `Deploy` workflow accepts these runs like push runs.
818
+
397
819
  ### Set up
398
820
 
399
821
  1. Make the package `ghcr.io/getsentry/roach` public once, after the first
@@ -447,6 +869,44 @@ To deploy by hand, restart the VM:
447
869
  `gcloud compute instances reset roach --zone <zone>`. It pulls `:main`,
448
870
  the last green commit.
449
871
 
872
+ The service reads its config only when it starts. When a change to the
873
+ service config must reach the VM before a change to the load balancer,
874
+ such as `proxyProtocol` before `proxy_header`, apply in this order. Run
875
+ the commands in `deploy/gcp`:
876
+
877
+ 1. Merge the change, and let the `Deploy` workflow deploy the image.
878
+ 2. Apply only the config secret:
879
+
880
+ ```sh
881
+ terraform apply -var-file=roach.tfvars \
882
+ -target=google_secret_manager_secret_version.config
883
+ ```
884
+
885
+ 3. Reset the VM, so that it reads the new config:
886
+
887
+ ```sh
888
+ gcloud compute instances reset roach --project roach-511216 --zone us-east1-b
889
+ ```
890
+
891
+ 4. When the service answers, apply the rest:
892
+
893
+ ```sh
894
+ terraform apply -var-file=roach.tfvars
895
+ ```
896
+
897
+ Step 1 and step 3 each reset the VM, and a reset ends the open runs, so
898
+ their CI jobs fail. Do step 3 when few runs are open, and do step 4 at
899
+ once after it.
900
+
901
+ With `proxyProtocol`, the service reads a PROXY header when a connection
902
+ has one, and it uses the address of the connection when it does not. The
903
+ load balancer writes its header before the first byte of the client, so a
904
+ client cannot put its own header first. A client can send its own header
905
+ only when the service has `proxyProtocol` and the load balancer does not
906
+ send the header: from step 3 to step 4 above. So do step 4 at once. A
907
+ false header changes only the address for the limit of runs for each
908
+ client, and the firewall lets only the Google ranges connect to the VM.
909
+
450
910
  To add a project, add its repository to the repositories of the
451
911
  `ROACH_TOKEN` organization secret, in the GitHub settings of the
452
912
  organization. Its CI jobs set `tenant` to `GITHUB_REPOSITORY`, and send
@@ -459,7 +919,7 @@ set the secret again, and restart the VM.
459
919
  ## Command line
460
920
 
461
921
  ```sh
462
- node src/cli.ts service <config.json>
922
+ node packages/service/src/cli.ts service <config.json>
463
923
  ```
464
924
 
465
925
  It starts the service and prints its URL as one JSON line.
@@ -472,20 +932,30 @@ The calls of a run are under `/__roach/runs/<id>`. They need
472
932
  - `POST /session` with `{"name": "..."}`: open a session. Returns
473
933
  `{"url": "..."}`, the proxy URL of the session. Its user is
474
934
  `<run id>-<session id>`. An open session with the same name fails
475
- first. A run can have 64 open sessions. Another one gets HTTP 429.
935
+ first. A run can have 64 open sessions. Another one gets HTTP 429. A
936
+ service before 0.2.0 returns no URL. Then `client.ts` fails and tells
937
+ you to deploy a newer service.
476
938
  - `POST /session/end` with `{"name": "...", "passed": true}`: end it.
477
939
  Returns `{"missed": 0}`, the misses that `replay` mode failed. Returns
478
- HTTP 409 when the session is not open.
940
+ HTTP 409 when the session is not open, and HTTP 500 when the store fails
941
+ (see "Errors" in [Service](#service)).
479
942
  - `GET /stats`: the totals of the run.
480
943
 
481
944
  The service also has:
482
945
 
483
946
  - `POST /__roach/runs` with a `RunConfig`: start a run. Send
484
947
  `Authorization: Bearer <write token>` for any mode but `replay`. Returns
485
- the `id`, `token`, proxy `url`, `controlUrl`, and `caCert` of the run.
948
+ HTTP 201 with the `id`, `token`, proxy `url`, `controlUrl`, and `caCert`
949
+ of the run. A body that is not JSON or a config that is not valid gets
950
+ HTTP 400, a body over 1 MiB gets HTTP 413, a mode other than
951
+ `replay` without the token gets HTTP 401, and a run past the limit of
952
+ runs without a token gets HTTP 429 (see [Limits](#limits)).
486
953
  - `DELETE /__roach/runs/<id>`: end the run. Returns its stats.
487
954
  - `GET /__roach/ca.pem`: the CA certificate. No token.
488
955
 
956
+ A call of a run with a wrong token gets HTTP 404, as a call of a run that
957
+ does not exist does.
958
+
489
959
  ## Release
490
960
 
491
961
  Run the `Release` workflow, and choose `minor`, `patch`, or `major`. Craft
@@ -497,11 +967,15 @@ manager accepts it, Craft:
497
967
  - makes the GitHub release `v<version>`, with the changelog, and moves the
498
968
  `v<major>` tag. Repositories use the action as `getsentry/roach@v<major>`.
499
969
  - publishes `@sentry/roach` to npm: the tarball that the `package` job of
500
- `CI` uploads for the release commit. Craft writes the version to
501
- `package.json` on the release branch.
970
+ `CI` uploads for the release commit. Craft publishes only
971
+ `sentry-roach-<version>.tgz`, and CI makes sure that it has `LICENSE` and
972
+ `README.md` (`.github/scripts/check-tarball.sh`). On the release branch, Craft runs
973
+ `.github/scripts/bump-version.sh`, which writes the version to
974
+ `packages/roach/package.json`. The workspace root and `packages/service`
975
+ are private and have no version.
502
976
 
503
- The action runs `src/action.ts` from the tag, and the package has the
504
- sources, so a release has no build step.
977
+ The action runs `packages/roach/src/action.ts` from the tag, and the
978
+ package has the sources, so a release has no build step.
505
979
 
506
980
  Craft takes the last version from the newest `v<version>` tag in the
507
981
  history of `main`. Do not rewrite `main`, or the tag leaves its history and
@@ -517,17 +991,24 @@ no image, and Craft cannot publish it.
517
991
 
518
992
  ```sh
519
993
  pnpm install
520
- pnpm check # format, lint, types, unused code, and tests, as CI runs them
994
+ pnpm check # format, lint, types, unused code, and tests
521
995
  ```
522
996
 
523
- `AGENTS.md` has the conventions and the commands for one file.
524
-
525
- - `tests/recording.test.ts` tests modes, sessions, changing values, and
526
- misses.
527
- - `tests/service.test.ts` tests the service in the test process.
528
- - `tests/vitest.test.ts` runs vitest with the integration on a small
997
+ CI runs the tests only with coverage (`pnpm test:coverage`), and packs
998
+ `@sentry/roach`. It does not run `pnpm check`, so run it before you push. `AGENTS.md` has the conventions and the commands for one
999
+ file.
1000
+
1001
+ - `packages/roach/tests/package.test.ts` checks what the npm package
1002
+ loads and ships.
1003
+ - `packages/service/tests/recording.test.ts` tests modes, sessions,
1004
+ changing values, and misses.
1005
+ - `packages/service/tests/redaction.test.ts` tests the redaction of
1006
+ credentials in recordings and request samples.
1007
+ - `packages/service/tests/service.test.ts` tests the service in the test
1008
+ process: access, limits, and the control API.
1009
+ - `packages/service/tests/vitest.test.ts` runs vitest with the integration on a small
529
1010
  project, against a service in the test process.
530
- - `tests/deployed.test.ts` runs the service as it runs in production: from
1011
+ - `packages/service/tests/deployed.test.ts` runs the service as it runs in production: from
531
1012
  the command line, behind a TLS server that does what the load balancer
532
1013
  does, with a local GCS server, metadata server, and Sentry server. Each
533
1014
  CI job runs the GitHub Action, and its command has only the proxy
@@ -535,28 +1016,61 @@ pnpm check # format, lint, types, unused code, and tests, as CI runs them
535
1016
 
536
1017
  ## Files
537
1018
 
538
- The proxy is in `src/`. Tests are in `tests/`. The production setup is in
539
- `deploy/gcp/` and `Dockerfile`.
1019
+ The repository is a pnpm workspace with two packages. The root package is
1020
+ private and has only the tools: oxlint, oxfmt, TypeScript, vitest, and knip.
1021
+ `pnpm-workspace.yaml` has the catalog of the versions that the packages
1022
+ share. The production setup is in `deploy/gcp/` and `Dockerfile`.
1023
+
1024
+ - `packages/roach` is the npm package `@sentry/roach`: the client, the
1025
+ vitest integration, and the action. It has no runtime dependencies, and it
1026
+ ships only `src/`. `tests/package.test.ts` fails when an export or the
1027
+ action loads a package that is not a Node built-in or vitest.
1028
+ - `packages/service` is the private package `@sentry/roach-service`: the
1029
+ shared service and the proxy. It imports `@sentry/roach` as a workspace
1030
+ package (`workspace:*`). It is the only package that depends on
1031
+ `@sentry/node`. Its tests run the client, the vitest integration, and the
1032
+ action against a real service.
1033
+
1034
+ In `packages/roach/src/`:
540
1035
 
541
1036
  - `types.ts`: rules, modes, runs, sessions, and the results of a run.
1037
+ - `values.ts`: changing values and their placeholders.
1038
+ - `client.ts`: starts a run and calls the control API.
1039
+ - `config.ts`: `roach.json`, for the action and vitest.
1040
+ - `vitest.ts`: the vitest global setup: one run for each invocation.
1041
+ - `vitest-setup.ts`: the vitest setup file: one session for each test.
1042
+ - `report.ts`: text reports of a run.
1043
+ - `action.ts`: the GitHub Action (`action.yml` at the root).
1044
+
1045
+ In `packages/service/src/`:
1046
+
1047
+ - `service.ts`: the shared service: tenants, runs, access, and metrics.
1048
+ - `cli.ts`: the command line, which the image runs.
542
1049
  - `server.ts`: sockets, HTTPS interception, the allow list, and the control
543
1050
  API.
1051
+ - `bodies.ts`: request bodies, their size limits, and the budget that the
1052
+ bodies of runs without the token share.
1053
+ - `clients.ts`: the PROXY protocol header and the address of a client.
1054
+ - `run-config.ts`: the check of the run config of a client, with its
1055
+ limits.
1056
+ - `errors.ts`: the errors that are the fault of the client, and the log
1057
+ line and the client text of an unexpected error.
544
1058
  - `addresses.ts`: the public addresses that the proxy can send requests to.
545
- - `service.ts`: the shared service: tenants, runs, access, and metrics.
546
1059
  - `recorder.ts`: modes, sessions, replay, and misses.
1060
+ - `recording-file.ts`: a recording file from a live response, the response
1061
+ of a replay, and the decoding of compressed bodies.
547
1062
  - `request-key.ts`: the key of a request.
548
- - `store.ts`: the recording format and the store contract.
1063
+ - `key-worker.ts`: the worker process that makes the keys of runs without
1064
+ the token and redacts the text of their misses, with a time limit and a
1065
+ memory limit.
1066
+ - `misses.ts`: request samples and the first change of a miss.
1067
+ - `store.ts`: the formats of recordings and request samples, and the
1068
+ store contract.
549
1069
  - `recordings.ts`: the file store, for tests and development.
550
1070
  - `gcs.ts`: the GCS store.
551
- - `values.ts`: changing values and their placeholders.
552
1071
  - `streams.ts`: merges the deltas of a recorded model stream.
553
1072
  - `usage.ts`: the token usage of a model response, for Sentry.
554
1073
  - `secrets.ts`: redaction of credentials.
1074
+ - `json-strings.ts`: the parse of a JSON body, and the one-pass scan of
1075
+ the JSON strings of a body, for `secrets.ts`.
555
1076
  - `certificates.ts`: the certificate authority for HTTPS.
556
- - `client.ts`: starts a run and calls the control API.
557
- - `config.ts`: `roach.json`, for the action and vitest.
558
- - `vitest.ts`: the vitest global setup: one run for each invocation.
559
- - `vitest-setup.ts`: the vitest setup file: one session for each test.
560
- - `report.ts`: text reports of a run.
561
- - `action.ts`: the GitHub Action (`action.yml` at the root).
562
- - `cli.ts`: the command line.