@sentry/roach 0.0.0-stage → 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/LICENSE +201 -0
- package/README.md +1075 -2
- package/package.json +43 -3
- package/src/action.ts +83 -0
- package/src/client.ts +234 -0
- package/src/config.ts +188 -0
- package/src/report.ts +86 -0
- package/src/types.ts +181 -0
- package/src/values.ts +267 -0
- package/src/vitest-setup.ts +62 -0
- package/src/vitest.ts +105 -0
package/README.md
CHANGED
|
@@ -1,3 +1,1076 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Roach
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Roach is a recording HTTPS proxy for tests. It records chosen requests and
|
|
4
|
+
replays them, so a second run of a test suite makes no live calls. The
|
|
5
|
+
proxy owns every read and write of the recordings. A test runner only gives
|
|
6
|
+
it rules and tells it when each test starts and ends.
|
|
7
|
+
|
|
8
|
+
Roach is a shared service (`packages/service`): one deployed proxy for many
|
|
9
|
+
projects and CI runs. A client points `HTTPS_PROXY` at it and trusts its
|
|
10
|
+
certificate authority. It installs nothing else. The recordings are in a
|
|
11
|
+
GCS bucket, which deletes each one 30 days after it was written. See
|
|
12
|
+
[Service](#service) and [Deploy](#deploy).
|
|
13
|
+
|
|
14
|
+
Roach started in
|
|
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.
|
|
20
|
+
|
|
21
|
+
## Use with vitest
|
|
22
|
+
|
|
23
|
+
Install `@sentry/roach`, add a `roach.json` (see [Config file](#config-file)),
|
|
24
|
+
and add two lines to the vitest config:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
export default defineConfig({
|
|
28
|
+
test: {
|
|
29
|
+
globalSetup: ["@sentry/roach/vitest"],
|
|
30
|
+
setupFiles: ["@sentry/roach/vitest/setup"],
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The global setup (`packages/roach/src/vitest.ts`) starts one run for the invocation and
|
|
36
|
+
gives its proxy variables to the test workers. The setup file
|
|
37
|
+
(`packages/roach/src/vitest-setup.ts`) opens one session for each test, so test files
|
|
38
|
+
record in parallel workers at the same time. A passed test writes its new
|
|
39
|
+
recordings. A failed test drops them. In `replay` mode, a test with a miss
|
|
40
|
+
fails. At the end, the global setup writes the stats of the run to stdout
|
|
41
|
+
and to the job summary. After you add Roach, do the check in
|
|
42
|
+
[Find changing values](#find-changing-values).
|
|
43
|
+
|
|
44
|
+
- `ROACH_TOKEN`: the write token. Only the global setup reads it. Without
|
|
45
|
+
it, the run can only replay, as in jobs of forks.
|
|
46
|
+
- `ROACH_URL`: the service. Default: `https://roach-proxy.getsentry.net`.
|
|
47
|
+
- `ROACH_MODE`: `auto`, `replay`, `record`, or `off`. Default: `auto` with
|
|
48
|
+
the token, `replay` without it. With `off`, the global setup starts no run,
|
|
49
|
+
so requests go direct and need no service and no token.
|
|
50
|
+
|
|
51
|
+
A project that has its own global setup calls `startRoach(project)` from
|
|
52
|
+
`@sentry/roach/vitest` in it, and returns its teardown.
|
|
53
|
+
|
|
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.
|
|
60
|
+
- The tests of one file share one process, so they run one at a time. Do
|
|
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.
|
|
69
|
+
- The package has TypeScript sources. Vitest compiles them, but Node does
|
|
70
|
+
not strip types in `node_modules`, so plain Node scripts cannot import it.
|
|
71
|
+
|
|
72
|
+
## Use the service
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { startRemoteRun, connectRoach } from "@sentry/roach/client";
|
|
76
|
+
import { describeRecordingStats } from "@sentry/roach/report";
|
|
77
|
+
import { VALUE_PATTERNS } from "@sentry/roach/values";
|
|
78
|
+
|
|
79
|
+
// Once per CI job. Without `token`, only `replay` mode is allowed.
|
|
80
|
+
const run = await startRemoteRun(
|
|
81
|
+
{ url: "https://roach.example.com", token: process.env.ROACH_TOKEN },
|
|
82
|
+
{
|
|
83
|
+
tenant: process.env.GITHUB_REPOSITORY!, // such as getsentry/junior
|
|
84
|
+
mode: "auto",
|
|
85
|
+
name: `${process.env.GITHUB_RUN_ID}-${process.env.GITHUB_RUN_ATTEMPT}`,
|
|
86
|
+
rules: [
|
|
87
|
+
{
|
|
88
|
+
name: "model",
|
|
89
|
+
match: { method: "POST", url: "https://ai-gateway.vercel.sh/" },
|
|
90
|
+
keyHeaders: ["ai-model-id"],
|
|
91
|
+
values: { uuid: VALUE_PATTERNS.uuid, time: VALUE_PATTERNS.isoTime },
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
spawn("vitest", { env: { ...process.env, ...run.env } });
|
|
97
|
+
|
|
98
|
+
// Once per test, in a worker that has the `controlUrl` and `token` of the
|
|
99
|
+
// run. Send the requests of the test through `session.url`.
|
|
100
|
+
const session = await connectRoach({ controlUrl, token }).startSession(
|
|
101
|
+
testName,
|
|
102
|
+
);
|
|
103
|
+
const { missed } = await session.end(passed);
|
|
104
|
+
// In `replay` mode, fail the test when `missed` is not 0.
|
|
105
|
+
|
|
106
|
+
// At the end of the job.
|
|
107
|
+
console.log(describeRecordingStats(await run.close()));
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`run.env` has `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`, `NODE_USE_ENV_PROXY`,
|
|
111
|
+
and `NODE_EXTRA_CA_CERTS`. Node 24 reads the last two only at startup. The
|
|
112
|
+
CA file also has the certificates of `NODE_EXTRA_CA_CERTS` of the caller.
|
|
113
|
+
A client that does not use `client.ts` can do the same with plain HTTP. See
|
|
114
|
+
[Control API](#control-api).
|
|
115
|
+
|
|
116
|
+
## Use the GitHub Action
|
|
117
|
+
|
|
118
|
+
A repository adds one step and a `roach.json` file (see
|
|
119
|
+
[Config file](#config-file)):
|
|
120
|
+
|
|
121
|
+
```yaml
|
|
122
|
+
- uses: getsentry/roach@v0
|
|
123
|
+
with:
|
|
124
|
+
token: ${{ secrets.ROACH_TOKEN }} # without it, the run can only replay
|
|
125
|
+
run: pnpm test
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The action (`packages/roach/src/action.ts`) starts a run with the token in `auto` mode, or
|
|
129
|
+
without it in `replay` mode. It runs `run` in bash with the proxy variables
|
|
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`.
|
|
132
|
+
|
|
133
|
+
- Only `run` uses the proxy. Later steps, such as cache and artifact
|
|
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.
|
|
137
|
+
- `run` is one session. When it fails, the run drops its new recordings. A
|
|
138
|
+
miss in `replay` mode fails the step. The summary names the job, not the
|
|
139
|
+
test, of each miss. For a session per test, use
|
|
140
|
+
[vitest](#use-with-vitest) in `run`.
|
|
141
|
+
- `NODE_EXTRA_CA_CERTS` works only for Node. Python and `curl` need a CA
|
|
142
|
+
bundle that also has the system certificates.
|
|
143
|
+
|
|
144
|
+
## Config file
|
|
145
|
+
|
|
146
|
+
The action and the vitest integration read `roach.json` in the working
|
|
147
|
+
directory or the closest parent directory (`packages/roach/src/config.ts`):
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
{
|
|
151
|
+
"allow": ["https://api.anthropic.com", "https://openrouter.ai"],
|
|
152
|
+
"rules": [
|
|
153
|
+
{
|
|
154
|
+
"name": "model",
|
|
155
|
+
"match": {
|
|
156
|
+
"method": "POST",
|
|
157
|
+
"url": [
|
|
158
|
+
"https://api.anthropic.com/v1/",
|
|
159
|
+
"https://openrouter.ai/api/v1/"
|
|
160
|
+
]
|
|
161
|
+
},
|
|
162
|
+
"values": {
|
|
163
|
+
"uuid": { "builtin": "uuid" },
|
|
164
|
+
"build": "build-[0-9]+"
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
- `allow` and `rules` are those of the run. See [Config](#config).
|
|
172
|
+
- A value is a regular expression source, or `{ "builtin": "<name>" }`
|
|
173
|
+
for a pattern of `VALUE_PATTERNS` in `values.ts`.
|
|
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).
|
|
179
|
+
- `tenant`: default `GITHUB_REPOSITORY`. Set it, so local runs use the
|
|
180
|
+
recordings of CI.
|
|
181
|
+
- `noProxy`: hosts that do not use the proxy. Default: loopback.
|
|
182
|
+
|
|
183
|
+
The `name` of the run is `<GITHUB_RUN_ID>-<GITHUB_RUN_ATTEMPT>` in GitHub
|
|
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.
|
|
186
|
+
|
|
187
|
+
## Config
|
|
188
|
+
|
|
189
|
+
A run takes `RunConfig` (`packages/roach/src/types.ts`): `tenant`, `mode`, `rules`,
|
|
190
|
+
`allow`, `name`, `secrets`, and `ci`.
|
|
191
|
+
|
|
192
|
+
- `mode`: `auto` replays recordings and records misses. `replay` replays
|
|
193
|
+
recordings and fails a miss with HTTP 412, so nothing goes live. `record`
|
|
194
|
+
sends every request live and records it again. `off` records and replays
|
|
195
|
+
nothing.
|
|
196
|
+
- `allow`: the only origins that the proxy sends requests to, 1 to 32 of
|
|
197
|
+
them. It refuses all other origins with HTTP 403. The upstream host
|
|
198
|
+
always comes from this list, not from the request. The service sends
|
|
199
|
+
requests only to public addresses, so an origin that resolves to a
|
|
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.
|
|
219
|
+
- Credentials: the proxy never writes the `secrets` of the service config
|
|
220
|
+
or of the run. Use `secrets` of the run for the credentials of a CI job
|
|
221
|
+
that a response can show but no request header has, such as a token that
|
|
222
|
+
goes to a model in a prompt. The service keeps them only in memory, for
|
|
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`.
|
|
227
|
+
The match is broad on purpose. It does not learn `x-session-affinity`,
|
|
228
|
+
because that header has a conversation id and not a credential.
|
|
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.
|
|
359
|
+
|
|
360
|
+
Requests that match no rule go live without a change, and their responses
|
|
361
|
+
stream. Each response has an `x-roach` header: `replayed`, `live`,
|
|
362
|
+
`missed`, or `passthrough`.
|
|
363
|
+
|
|
364
|
+
## Recordings
|
|
365
|
+
|
|
366
|
+
The key of a recording is `<rule>/<hash>.json`, under `<tenant>/` in the
|
|
367
|
+
store. The hash is the hash of the rule name, the method, the URL, the key
|
|
368
|
+
headers, and the body. A JSON body has sorted keys. A recording keeps the
|
|
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).
|
|
373
|
+
|
|
374
|
+
- Many sessions can be open at once. A request belongs to the session of
|
|
375
|
+
its proxy URL (`url` of the session). A request through the proxy URL of
|
|
376
|
+
the run belongs to the open session when only one is open. The proxy
|
|
377
|
+
keeps the new recordings of a session in memory. A passed session writes them, and a failed session
|
|
378
|
+
drops them, so a bad sample is never replayed.
|
|
379
|
+
- A request belongs to the session that was open when it started. If it
|
|
380
|
+
ends after its session ended, it follows the result of that session.
|
|
381
|
+
- A request outside a session is written at once.
|
|
382
|
+
- A 429 or 5xx response is never recorded. A replay does not write the
|
|
383
|
+
file again.
|
|
384
|
+
|
|
385
|
+
## Changing values
|
|
386
|
+
|
|
387
|
+
Ids and times change on each run, and a response often repeats them. For
|
|
388
|
+
example, the model archives the memory with the id that its request
|
|
389
|
+
showed. `values` of a rule maps a name to a regular expression source.
|
|
390
|
+
`VALUE_PATTERNS` in `packages/roach/src/values.ts` has `uuid`, `isoTime`, `date`, `epochMs`,
|
|
391
|
+
`sha256`, and `gitCommit`. When two patterns match at the same place, the first one wins.
|
|
392
|
+
The proxy matches values in JSON text. Start a custom pattern with
|
|
393
|
+
`NOT_AFTER_WORD`, not `\b`: after an escape such as `\n`, `\b` sees the `n`
|
|
394
|
+
as part of a word and does not match.
|
|
395
|
+
|
|
396
|
+
- The key sees each value as `<<name>>`. So two runs with other ids or
|
|
397
|
+
times use the same recording.
|
|
398
|
+
- When the proxy records a response, it writes each value of the request as
|
|
399
|
+
`<<name:n>>`: the n-th value of that name in the request.
|
|
400
|
+
- On replay, the proxy writes the n-th value of the current request there.
|
|
401
|
+
A replayed response thus uses the ids of this run.
|
|
402
|
+
- A value that the response makes itself, such as a date that a model
|
|
403
|
+
calculates, stays as it was recorded.
|
|
404
|
+
- A session remembers the values of its requests. In a later request of
|
|
405
|
+
the same session, such a value is also a placeholder where no pattern
|
|
406
|
+
finds it. For example, a pattern finds a short commit id only in the
|
|
407
|
+
output of `git push`, but the model then quotes it alone in its reply. A
|
|
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.
|
|
414
|
+
- Values in thinking blocks of the request do not count for `n`, and the
|
|
415
|
+
proxy does not look for remembered values in them. A replay keeps the
|
|
416
|
+
recorded text of a thinking block, so when a later request sends it back,
|
|
417
|
+
its values come from the recording run.
|
|
418
|
+
|
|
419
|
+
Model streams send a tool call in many small deltas, so a value can be
|
|
420
|
+
split over two events. Before it records a `text/event-stream`, the proxy
|
|
421
|
+
merges the deltas of each Anthropic Messages content block into one event.
|
|
422
|
+
Thinking blocks keep their recorded text, because their signature covers
|
|
423
|
+
it, and the provider refuses a changed thinking block.
|
|
424
|
+
|
|
425
|
+
Keep the patterns narrow. A broad pattern makes requests that differ in a
|
|
426
|
+
real way use the same recording.
|
|
427
|
+
|
|
428
|
+
The service takes any pattern that compiles in 1 s, up to 2048
|
|
429
|
+
characters. A run without the token must use the same patterns as the run
|
|
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
|
+
|
|
463
|
+
Avoid nested quantifiers, such as `(a+)+`.
|
|
464
|
+
|
|
465
|
+
## Misses
|
|
466
|
+
|
|
467
|
+
The proxy adds each request without a recording to `stats().misses`, with
|
|
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.
|
|
596
|
+
|
|
597
|
+
## Service
|
|
598
|
+
|
|
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.
|
|
601
|
+
|
|
602
|
+
- **Tenants.** A tenant is a GitHub repository, as `owner/repo`, such as
|
|
603
|
+
`getsentry/junior`. In GitHub Actions, use `GITHUB_REPOSITORY`. The
|
|
604
|
+
service has no list of tenants, so a new project needs no change to the
|
|
605
|
+
service. The recordings of a tenant are under `<owner>/<repo>/` in the
|
|
606
|
+
bucket. Tenants never share recordings.
|
|
607
|
+
- **One write token.** All tenants share one write token. The config keeps
|
|
608
|
+
only its SHA-256, in `writeTokenHash`. The tenant name is only a label,
|
|
609
|
+
not proof of identity: a job with the token can write the recordings of
|
|
610
|
+
any tenant. So give the token only to repositories and workflows that
|
|
611
|
+
you trust, and never to a workflow that runs code from a fork.
|
|
612
|
+
- **Reads are public.** Anyone can start a `replay` run of a tenant, so CI
|
|
613
|
+
jobs of forks replay without a secret. Such a run never writes, and it
|
|
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).
|
|
619
|
+
- **Runs.** Each run has its own id, token, mode, rules, sessions, and
|
|
620
|
+
stats. A proxied request names its run in `Proxy-Authorization`, as
|
|
621
|
+
`Basic base64(<id>:<token>)`. The proxy URL of the run has them, so
|
|
622
|
+
`HTTPS_PROXY` sends them. A request without them gets HTTP 407.
|
|
623
|
+
- **Config of a run.** Each run sends its own `allow` and `rules`, with
|
|
624
|
+
their value patterns. A tenant that calls a new origin or uses a new
|
|
625
|
+
pattern changes only its own config. The service needs no change and no
|
|
626
|
+
restart. Two checks keep one run from harming the others:
|
|
627
|
+
- The proxy sends requests only to public addresses
|
|
628
|
+
(`packages/service/src/addresses.ts`).
|
|
629
|
+
It checks the address of each connection after DNS, so a host name
|
|
630
|
+
cannot point a run at the metadata server, at the VPC, or at the VM.
|
|
631
|
+
Only tests and local development set `privateNetworks`.
|
|
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.
|
|
636
|
+
- **Sentry.** With `sentryDsn`, each read and write of a recording sends
|
|
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.
|
|
646
|
+
- **Certificate authority.** `ca` holds the PEM certificate and key that
|
|
647
|
+
clients trust. Keep it across restarts. Host certificates live for 7
|
|
648
|
+
days, and the service makes new ones before they expire.
|
|
649
|
+
|
|
650
|
+
### Sentry
|
|
651
|
+
|
|
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).
|
|
671
|
+
- `roach.request`: one proxied request, named `<method> <origin>`, with
|
|
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.
|
|
709
|
+
From a model call, it has only the model name and the token counts.
|
|
710
|
+
So the service turns off the request data and the HTTP spans of the SDK,
|
|
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.
|
|
718
|
+
|
|
719
|
+
### Limits
|
|
720
|
+
|
|
721
|
+
- Runs live in memory in one process. A restart ends the open runs, and
|
|
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.
|
|
729
|
+
- The service can have 100 open runs without a token, for all tenants
|
|
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.
|
|
774
|
+
- GCS deletes each recording 30 days after it was written, also when runs
|
|
775
|
+
still replay it. Then `auto` mode records it again, and `replay` mode
|
|
776
|
+
fails the request with HTTP 412.
|
|
777
|
+
- A write replaces the recording with the same key.
|
|
778
|
+
|
|
779
|
+
## Deploy
|
|
780
|
+
|
|
781
|
+
`deploy/gcp/` is the production setup, in Terraform. It makes:
|
|
782
|
+
|
|
783
|
+
- a GCS bucket that deletes each recording after `recording_days` (30),
|
|
784
|
+
- the certificate authority and the write token,
|
|
785
|
+
- the service config, in Secret Manager,
|
|
786
|
+
- one Container-Optimized OS VM that runs the image
|
|
787
|
+
`ghcr.io/getsentry/roach:main`, with a service account that can only use
|
|
788
|
+
the bucket and the secret,
|
|
789
|
+
- an SSL proxy load balancer with a Google-managed certificate. It ends TLS
|
|
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,
|
|
795
|
+
- a service account for the `Deploy` workflow, which GitHub Actions uses
|
|
796
|
+
through workload identity federation (`deploy.tf`). It can only reset
|
|
797
|
+
the VM.
|
|
798
|
+
|
|
799
|
+
The `Image` workflow builds the image on each pull request and pushes it as
|
|
800
|
+
`:<sha>` on `main`. The VM pulls `:main` at each start.
|
|
801
|
+
|
|
802
|
+
The `Deploy` workflow deploys `main` when it is green. After the `Image`
|
|
803
|
+
workflow of a commit on `main`, it waits for `CI` of the same commit. When
|
|
804
|
+
both pass and the commit is still the head of `main`, it moves `:main` to
|
|
805
|
+
`:<sha>`, resets the VM, and waits until the service answers. A reset ends
|
|
806
|
+
the open runs, so their CI jobs fail. A red commit does not deploy, and an
|
|
807
|
+
older commit never replaces a newer one.
|
|
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
|
+
|
|
819
|
+
### Set up
|
|
820
|
+
|
|
821
|
+
1. Make the package `ghcr.io/getsentry/roach` public once, after the first
|
|
822
|
+
push to `main`, so the VM can pull it without credentials.
|
|
823
|
+
2. `deploy/gcp/roach.tfvars` has the values of the Sentry deployment: the
|
|
824
|
+
GCP project `roach-511216`, the domain, and the DSN of the `sentry/roach`
|
|
825
|
+
project. Another deployment changes these values. It has no origins or
|
|
826
|
+
patterns: runs send them.
|
|
827
|
+
3. The Terraform state is in the private bucket that `backend "gcs"` in
|
|
828
|
+
`versions.tf` names. The state holds the CA key and the write token, so
|
|
829
|
+
the bucket must stay private. Make the bucket before the first
|
|
830
|
+
`terraform init`.
|
|
831
|
+
4. Log in with `gcloud auth application-default login`, then apply:
|
|
832
|
+
|
|
833
|
+
```sh
|
|
834
|
+
cd deploy/gcp
|
|
835
|
+
terraform init
|
|
836
|
+
terraform apply -var-file=roach.tfvars
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
5. Point an A record of `domain` at the `ip_address` output. The
|
|
840
|
+
certificate works some minutes after DNS does.
|
|
841
|
+
6. Put the write token in one GitHub organization secret, `ROACH_TOKEN`.
|
|
842
|
+
Give it access only to the repositories that you trust. This command
|
|
843
|
+
does not show the token:
|
|
844
|
+
|
|
845
|
+
```sh
|
|
846
|
+
terraform output -raw write_token \
|
|
847
|
+
| gh secret set ROACH_TOKEN --org getsentry --visibility selected \
|
|
848
|
+
--repos getsentry/junior
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
7. Check it: `curl https://<domain>/__roach/ca.pem` returns the CA
|
|
852
|
+
certificate.
|
|
853
|
+
8. Let the `Deploy` workflow reset the VM. Set two variables of the
|
|
854
|
+
repository from the outputs. Until they are set, the workflow only
|
|
855
|
+
moves `:main`, and the VM gets it at its next start:
|
|
856
|
+
|
|
857
|
+
```sh
|
|
858
|
+
gh variable set GCP_WORKLOAD_IDENTITY_PROVIDER --repo getsentry/roach \
|
|
859
|
+
--body "$(terraform output -raw deploy_workload_identity_provider)"
|
|
860
|
+
gh variable set GCP_DEPLOY_SERVICE_ACCOUNT --repo getsentry/roach \
|
|
861
|
+
--body "$(terraform output -raw deploy_service_account)"
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
Another deployment also changes `deploy_repository` and
|
|
865
|
+
`deploy_repository_id` in its tfvars, and the `env` of
|
|
866
|
+
`.github/workflows/deploy.yml`.
|
|
867
|
+
|
|
868
|
+
To deploy by hand, restart the VM:
|
|
869
|
+
`gcloud compute instances reset roach --zone <zone>`. It pulls `:main`,
|
|
870
|
+
the last green commit.
|
|
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
|
+
|
|
910
|
+
To add a project, add its repository to the repositories of the
|
|
911
|
+
`ROACH_TOKEN` organization secret, in the GitHub settings of the
|
|
912
|
+
organization. Its CI jobs set `tenant` to `GITHUB_REPOSITORY`, and send
|
|
913
|
+
their own origins and patterns. The service needs no change.
|
|
914
|
+
|
|
915
|
+
To change the write token, run
|
|
916
|
+
`terraform apply -var-file=roach.tfvars -replace=random_password.write_token`,
|
|
917
|
+
set the secret again, and restart the VM.
|
|
918
|
+
|
|
919
|
+
## Command line
|
|
920
|
+
|
|
921
|
+
```sh
|
|
922
|
+
node packages/service/src/cli.ts service <config.json>
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
It starts the service and prints its URL as one JSON line.
|
|
926
|
+
|
|
927
|
+
## Control API
|
|
928
|
+
|
|
929
|
+
The calls of a run are under `/__roach/runs/<id>`. They need
|
|
930
|
+
`Authorization: Bearer <run token>`. `client.ts` calls them.
|
|
931
|
+
|
|
932
|
+
- `POST /session` with `{"name": "..."}`: open a session. Returns
|
|
933
|
+
`{"url": "..."}`, the proxy URL of the session. Its user is
|
|
934
|
+
`<run id>-<session id>`. An open session with the same name fails
|
|
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.
|
|
938
|
+
- `POST /session/end` with `{"name": "...", "passed": true}`: end it.
|
|
939
|
+
Returns `{"missed": 0}`, the misses that `replay` mode failed. Returns
|
|
940
|
+
HTTP 409 when the session is not open, and HTTP 500 when the store fails
|
|
941
|
+
(see "Errors" in [Service](#service)).
|
|
942
|
+
- `GET /stats`: the totals of the run.
|
|
943
|
+
|
|
944
|
+
The service also has:
|
|
945
|
+
|
|
946
|
+
- `POST /__roach/runs` with a `RunConfig`: start a run. Send
|
|
947
|
+
`Authorization: Bearer <write token>` for any mode but `replay`. Returns
|
|
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)).
|
|
953
|
+
- `DELETE /__roach/runs/<id>`: end the run. Returns its stats.
|
|
954
|
+
- `GET /__roach/ca.pem`: the CA certificate. No token.
|
|
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
|
+
|
|
959
|
+
## Release
|
|
960
|
+
|
|
961
|
+
Run the `Release` workflow, and choose `minor`, `patch`, or `major`. Craft
|
|
962
|
+
(`.craft.yml`) makes a `release/<version>` branch and asks for approval in
|
|
963
|
+
[getsentry/publish](https://github.com/getsentry/publish). When a release
|
|
964
|
+
manager accepts it, Craft:
|
|
965
|
+
|
|
966
|
+
- copies the image `ghcr.io/getsentry/roach:<sha>` to `:<version>`.
|
|
967
|
+
- makes the GitHub release `v<version>`, with the changelog, and moves the
|
|
968
|
+
`v<major>` tag. Repositories use the action as `getsentry/roach@v<major>`.
|
|
969
|
+
- publishes `@sentry/roach` to npm: the tarball that the `package` job of
|
|
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.
|
|
976
|
+
|
|
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.
|
|
979
|
+
|
|
980
|
+
Craft takes the last version from the newest `v<version>` tag in the
|
|
981
|
+
history of `main`. Do not rewrite `main`, or the tag leaves its history and
|
|
982
|
+
Craft bumps from 0.0.0. The workflow deletes the floating `v<major>` tags in
|
|
983
|
+
its checkout first (`.github/scripts/drop-floating-tags.sh`), because
|
|
984
|
+
`git describe` picks them over the release tag on the same commit.
|
|
985
|
+
|
|
986
|
+
The `Release` workflow uses the token of Sentry Release Bot. A push with
|
|
987
|
+
`GITHUB_TOKEN` starts no workflows, so the release branch gets no checks and
|
|
988
|
+
no image, and Craft cannot publish it.
|
|
989
|
+
|
|
990
|
+
## Development
|
|
991
|
+
|
|
992
|
+
```sh
|
|
993
|
+
pnpm install
|
|
994
|
+
pnpm check # format, lint, types, unused code, and tests
|
|
995
|
+
```
|
|
996
|
+
|
|
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
|
|
1010
|
+
project, against a service in the test process.
|
|
1011
|
+
- `packages/service/tests/deployed.test.ts` runs the service as it runs in production: from
|
|
1012
|
+
the command line, behind a TLS server that does what the load balancer
|
|
1013
|
+
does, with a local GCS server, metadata server, and Sentry server. Each
|
|
1014
|
+
CI job runs the GitHub Action, and its command has only the proxy
|
|
1015
|
+
variables of its run.
|
|
1016
|
+
|
|
1017
|
+
## Files
|
|
1018
|
+
|
|
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/`:
|
|
1035
|
+
|
|
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.
|
|
1049
|
+
- `server.ts`: sockets, HTTPS interception, the allow list, and the control
|
|
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.
|
|
1058
|
+
- `addresses.ts`: the public addresses that the proxy can send requests to.
|
|
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.
|
|
1062
|
+
- `request-key.ts`: the key of a request.
|
|
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.
|
|
1069
|
+
- `recordings.ts`: the file store, for tests and development.
|
|
1070
|
+
- `gcs.ts`: the GCS store.
|
|
1071
|
+
- `streams.ts`: merges the deltas of a recorded model stream.
|
|
1072
|
+
- `usage.ts`: the token usage of a model response, for Sentry.
|
|
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`.
|
|
1076
|
+
- `certificates.ts`: the certificate authority for HTTPS.
|