realuptime-errors 0.1.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.
@@ -0,0 +1,506 @@
1
+ {
2
+ "comment": "Shared PII-scrub test vectors for the Errors SDKs and the server-side second net (docs/errors-plan.md, 'The SDKs: two languages, one contract'). Consumed by packages/errors-js/scrub.test.ts, packages/errors-py/tests/test_scrub.py, and packages/db/error-scrub.test.ts. All three implementations must produce byte-identical output for every vector; a semantic change here is a wire-contract change and needs all three updated in one PR. The server's second net applies the PATTERN rules only: FIELD REMOVAL (removed headers, and v2's user.email / user.username) and allowFields are client-side duties -- a field that reaches the server was either never in the removal set or was deliberately opted back in, and the server cannot tell which. Vectors where the server's output therefore differs carry expectedServer; absent it, the server expects `expected`. Vectors named 'v2 ...' cover the REA-182 additions (identity, tags, custom context, device metadata, matched route, frame locals); every v1 vector above them is unchanged, which is itself part of the additive guarantee.",
3
+ "version": 2,
4
+ "vectors": [
5
+ {
6
+ "name": "card number, plain, Luhn-valid",
7
+ "event": { "message": "Payment failed for 4242424242424242" },
8
+ "expected": { "message": "Payment failed for [scrubbed]" }
9
+ },
10
+ {
11
+ "name": "card number with separators, Luhn-valid",
12
+ "event": { "message": "card 4111 1111 1111 1111 declined" },
13
+ "expected": { "message": "card [scrubbed] declined" }
14
+ },
15
+ {
16
+ "name": "16 digits failing Luhn stay (an order id is not a card)",
17
+ "event": { "message": "order 4242424242424241 not found" },
18
+ "expected": { "message": "order 4242424242424241 not found" }
19
+ },
20
+ {
21
+ "name": "short digit runs stay",
22
+ "event": { "message": "HTTP 502 after 12345 ms" },
23
+ "expected": { "message": "HTTP 502 after 12345 ms" }
24
+ },
25
+ {
26
+ "name": "amex-length card, Luhn-valid",
27
+ "event": { "message": "378282246310005 charged twice" },
28
+ "expected": { "message": "[scrubbed] charged twice" }
29
+ },
30
+ {
31
+ "name": "JWT three-dot shape",
32
+ "event": { "message": "token eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJVadQssw5c expired" },
33
+ "expected": { "message": "token [scrubbed] expired" }
34
+ },
35
+ {
36
+ "name": "prefixed secret key (sk_)",
37
+ "event": { "message": "sk_live_abcdef1234567890 rejected" },
38
+ "expected": { "message": "[scrubbed] rejected" }
39
+ },
40
+ {
41
+ "name": "prefixed token (ghp_)",
42
+ "event": { "message": "push denied for ghp_AbCdEf1234567890abcdef" },
43
+ "expected": { "message": "push denied for [scrubbed]" }
44
+ },
45
+ {
46
+ "name": "realuptime agent token (rua_)",
47
+ "event": { "message": "agent auth failed: rua_0123456789abcdef0123456789abcdef" },
48
+ "expected": { "message": "agent auth failed: [scrubbed]" }
49
+ },
50
+ {
51
+ "name": "long hex run (32+)",
52
+ "event": { "message": "session deadbeefdeadbeefdeadbeefdeadbeef invalid" },
53
+ "expected": { "message": "session [scrubbed] invalid" }
54
+ },
55
+ {
56
+ "name": "short hex run stays (16)",
57
+ "event": { "message": "trace deadbeefdeadbeef sampled" },
58
+ "expected": { "message": "trace deadbeefdeadbeef sampled" }
59
+ },
60
+ {
61
+ "name": "long base64-ish run (40+)",
62
+ "event": { "message": "blob QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVphYmNkZWZnaGlq= rejected" },
63
+ "expected": { "message": "blob [scrubbed] rejected" }
64
+ },
65
+ {
66
+ "name": "authorization header removed, benign header kept",
67
+ "event": {
68
+ "message": "request failed",
69
+ "request": {
70
+ "method": "GET",
71
+ "path": "/orders",
72
+ "status": 500,
73
+ "headers": { "authorization": "Bearer some-session-value", "x-request-id": "req-42" }
74
+ }
75
+ },
76
+ "expected": {
77
+ "message": "request failed",
78
+ "request": {
79
+ "method": "GET",
80
+ "path": "/orders",
81
+ "status": 500,
82
+ "headers": { "authorization": "[scrubbed]", "x-request-id": "req-42" }
83
+ }
84
+ },
85
+ "expectedServer": {
86
+ "message": "request failed",
87
+ "request": {
88
+ "method": "GET",
89
+ "path": "/orders",
90
+ "status": 500,
91
+ "headers": { "authorization": "Bearer some-session-value", "x-request-id": "req-42" }
92
+ }
93
+ }
94
+ },
95
+ {
96
+ "name": "cookie and set-cookie removed, any casing",
97
+ "event": {
98
+ "message": "boom",
99
+ "request": {
100
+ "method": "POST",
101
+ "path": "/login",
102
+ "status": 500,
103
+ "headers": { "Cookie": "sid=abc", "Set-Cookie": "sid=def", "Proxy-Authorization": "Basic xyz" }
104
+ }
105
+ },
106
+ "expected": {
107
+ "message": "boom",
108
+ "request": {
109
+ "method": "POST",
110
+ "path": "/login",
111
+ "status": 500,
112
+ "headers": { "Cookie": "[scrubbed]", "Set-Cookie": "[scrubbed]", "Proxy-Authorization": "[scrubbed]" }
113
+ }
114
+ },
115
+ "expectedServer": {
116
+ "message": "boom",
117
+ "request": {
118
+ "method": "POST",
119
+ "path": "/login",
120
+ "status": 500,
121
+ "headers": { "Cookie": "sid=abc", "Set-Cookie": "sid=def", "Proxy-Authorization": "Basic xyz" }
122
+ }
123
+ }
124
+ },
125
+ {
126
+ "name": "kept header values still pass the pattern scrub",
127
+ "event": {
128
+ "message": "boom",
129
+ "request": {
130
+ "method": "GET",
131
+ "path": "/v1/charge",
132
+ "status": 500,
133
+ "headers": { "x-api-key": "sk_test_abcdefgh12345678" }
134
+ }
135
+ },
136
+ "expected": {
137
+ "message": "boom",
138
+ "request": {
139
+ "method": "GET",
140
+ "path": "/v1/charge",
141
+ "status": 500,
142
+ "headers": { "x-api-key": "[scrubbed]" }
143
+ }
144
+ }
145
+ },
146
+ {
147
+ "name": "allowFields opts one header back in, by lowercased name",
148
+ "allowFields": ["authorization"],
149
+ "event": {
150
+ "message": "boom",
151
+ "request": {
152
+ "method": "GET",
153
+ "path": "/me",
154
+ "status": 401,
155
+ "headers": { "Authorization": "Digest username=alice" }
156
+ }
157
+ },
158
+ "expected": {
159
+ "message": "boom",
160
+ "request": {
161
+ "method": "GET",
162
+ "path": "/me",
163
+ "status": 401,
164
+ "headers": { "Authorization": "Digest username=alice" }
165
+ }
166
+ }
167
+ },
168
+ {
169
+ "name": "allowFields never disables the pattern scrub on the kept value",
170
+ "allowFields": ["authorization"],
171
+ "event": {
172
+ "message": "boom",
173
+ "request": {
174
+ "method": "GET",
175
+ "path": "/me",
176
+ "status": 401,
177
+ "headers": { "authorization": "Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJVadQssw5c" }
178
+ }
179
+ },
180
+ "expected": {
181
+ "message": "boom",
182
+ "request": {
183
+ "method": "GET",
184
+ "path": "/me",
185
+ "status": 401,
186
+ "headers": { "authorization": "Bearer [scrubbed]" }
187
+ }
188
+ }
189
+ },
190
+ {
191
+ "name": "token in the request path is scrubbed",
192
+ "event": {
193
+ "message": "boom",
194
+ "request": {
195
+ "method": "GET",
196
+ "path": "/reset/eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJVadQssw5c",
197
+ "status": 404
198
+ }
199
+ },
200
+ "expected": {
201
+ "message": "boom",
202
+ "request": { "method": "GET", "path": "/reset/[scrubbed]", "status": 404 }
203
+ }
204
+ },
205
+ {
206
+ "name": "frames and tags are left alone; only message and request are scrubbed",
207
+ "event": {
208
+ "message": "TypeError: x is undefined",
209
+ "exceptionType": "TypeError",
210
+ "release": "deadbeefdeadbeefdeadbeefdeadbeef",
211
+ "environment": "production",
212
+ "frames": [
213
+ { "file": "src/checkout.ts", "function": "processOrder", "line": 42, "inApp": true }
214
+ ]
215
+ },
216
+ "expected": {
217
+ "message": "TypeError: x is undefined",
218
+ "exceptionType": "TypeError",
219
+ "release": "deadbeefdeadbeefdeadbeefdeadbeef",
220
+ "environment": "production",
221
+ "frames": [
222
+ { "file": "src/checkout.ts", "function": "processOrder", "line": 42, "inApp": true }
223
+ ]
224
+ }
225
+ },
226
+ {
227
+ "name": "multiple secrets in one message are each scrubbed",
228
+ "event": { "message": "retry with sk_live_abcdefgh12345678 after 4242424242424242 failed" },
229
+ "expected": { "message": "retry with [scrubbed] after [scrubbed] failed" }
230
+ },
231
+ {
232
+ "name": "breadcrumb messages pass the pattern scrub (Phase 3)",
233
+ "event": {
234
+ "message": "boom",
235
+ "breadcrumbs": [
236
+ { "timestamp": null, "category": "http", "message": "POST /charge with sk_live_abcdefgh12345678", "data": null }
237
+ ]
238
+ },
239
+ "expected": {
240
+ "message": "boom",
241
+ "breadcrumbs": [
242
+ { "timestamp": null, "category": "http", "message": "POST /charge with [scrubbed]", "data": null }
243
+ ]
244
+ }
245
+ },
246
+ {
247
+ "name": "breadcrumb data values pass the pattern scrub; benign values stay",
248
+ "event": {
249
+ "message": "boom",
250
+ "breadcrumbs": [
251
+ {
252
+ "timestamp": null,
253
+ "category": "payment",
254
+ "message": "charging card",
255
+ "data": { "card": "4242424242424242", "order": "ord-42" }
256
+ }
257
+ ]
258
+ },
259
+ "expected": {
260
+ "message": "boom",
261
+ "breadcrumbs": [
262
+ {
263
+ "timestamp": null,
264
+ "category": "payment",
265
+ "message": "charging card",
266
+ "data": { "card": "[scrubbed]", "order": "ord-42" }
267
+ }
268
+ ]
269
+ }
270
+ },
271
+ {
272
+ "name": "frame source-context lines pass the pattern scrub; file, function and line stay",
273
+ "event": {
274
+ "message": "boom",
275
+ "frames": [
276
+ {
277
+ "file": "app/billing.py",
278
+ "function": "charge",
279
+ "line": 12,
280
+ "inApp": true,
281
+ "contextLine": "API_KEY = \"sk_live_abcdefgh12345678\"",
282
+ "preContext": ["import stripe"],
283
+ "postContext": ["charge(API_KEY)"]
284
+ }
285
+ ]
286
+ },
287
+ "expected": {
288
+ "message": "boom",
289
+ "frames": [
290
+ {
291
+ "file": "app/billing.py",
292
+ "function": "charge",
293
+ "line": 12,
294
+ "inApp": true,
295
+ "contextLine": "API_KEY = \"[scrubbed]\"",
296
+ "preContext": ["import stripe"],
297
+ "postContext": ["charge(API_KEY)"]
298
+ }
299
+ ]
300
+ }
301
+ },
302
+ {
303
+ "name": "Django-style request path with a card number segment is scrubbed (REA-184 Django middleware)",
304
+ "event": {
305
+ "message": "boom",
306
+ "request": { "method": "GET", "path": "/orders/4111111111111111/receipt", "status": 500 }
307
+ },
308
+ "expected": {
309
+ "message": "boom",
310
+ "request": { "method": "GET", "path": "/orders/[scrubbed]/receipt", "status": 500 }
311
+ }
312
+ },
313
+ {
314
+ "name": "FastAPI/Starlette route-template path carrying a prefixed key param is scrubbed (REA-184 FastAPI middleware)",
315
+ "event": {
316
+ "message": "webhook delivery failed",
317
+ "request": { "method": "POST", "path": "/webhooks/sk_live_abcdefgh12345678/deliver", "status": 500 }
318
+ },
319
+ "expected": {
320
+ "message": "webhook delivery failed",
321
+ "request": { "method": "POST", "path": "/webhooks/[scrubbed]/deliver", "status": 500 }
322
+ }
323
+ },
324
+ {
325
+ "name": "Celery task-failure message embedding a card number is scrubbed (REA-184 Celery signal hook)",
326
+ "event": {
327
+ "message": "charge 4242424242424242 failed in task process_payment",
328
+ "fingerprint": ["celery", "app.tasks.process_payment", "ValueError"]
329
+ },
330
+ "expected": {
331
+ "message": "charge [scrubbed] failed in task process_payment",
332
+ "fingerprint": ["celery", "app.tasks.process_payment", "ValueError"]
333
+ }
334
+ },
335
+ {
336
+ "name": "Express default capture has no headers at all; a benign path with a short digit run stays untouched (REA-184 Express middleware)",
337
+ "event": {
338
+ "message": "checkout failed",
339
+ "request": { "method": "POST", "path": "/checkout/session/8842", "status": 500 }
340
+ },
341
+ "expected": {
342
+ "message": "checkout failed",
343
+ "request": { "method": "POST", "path": "/checkout/session/8842", "status": 500 }
344
+ }
345
+ },
346
+
347
+ {
348
+ "name": "v2 identity: user.id rides as-is, email and username are removed by default (REA-182)",
349
+ "event": {
350
+ "message": "checkout failed",
351
+ "user": { "id": "acct_9182", "email": "alice@example.com", "username": "alice" }
352
+ },
353
+ "expected": {
354
+ "message": "checkout failed",
355
+ "user": { "id": "acct_9182", "email": "[scrubbed]", "username": "[scrubbed]" }
356
+ },
357
+ "expectedServer": {
358
+ "message": "checkout failed",
359
+ "user": { "id": "acct_9182", "email": "alice@example.com", "username": "alice" }
360
+ }
361
+ },
362
+ {
363
+ "name": "v2 identity: allowFields opts user.email back in by exact dotted name; username stays removed",
364
+ "allowFields": ["user.email"],
365
+ "event": {
366
+ "message": "checkout failed",
367
+ "user": { "id": "acct_9182", "email": "alice@example.com", "username": "alice" }
368
+ },
369
+ "expected": {
370
+ "message": "checkout failed",
371
+ "user": { "id": "acct_9182", "email": "alice@example.com", "username": "[scrubbed]" }
372
+ },
373
+ "expectedServer": {
374
+ "message": "checkout failed",
375
+ "user": { "id": "acct_9182", "email": "alice@example.com", "username": "alice" }
376
+ }
377
+ },
378
+ {
379
+ "name": "v2 identity: an opted-in user.email still passes the pattern scrub",
380
+ "allowFields": ["user.email"],
381
+ "event": {
382
+ "message": "boom",
383
+ "user": { "email": "alice+eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJVadQssw5c@example.com" }
384
+ },
385
+ "expected": {
386
+ "message": "boom",
387
+ "user": { "email": "alice+[scrubbed]@example.com" }
388
+ }
389
+ },
390
+ {
391
+ "name": "v2 identity: a token-shaped user.id is pattern-scrubbed like any other string",
392
+ "event": { "message": "boom", "user": { "id": "sk_live_abcdefgh12345678" } },
393
+ "expected": { "message": "boom", "user": { "id": "[scrubbed]" } }
394
+ },
395
+ {
396
+ "name": "v2 tags: values pass the pattern scrub, keys and benign values stay",
397
+ "event": {
398
+ "message": "charge failed",
399
+ "tags": { "tenant": "acme", "plan": "growth", "card": "4242424242424242" }
400
+ },
401
+ "expected": {
402
+ "message": "charge failed",
403
+ "tags": { "tenant": "acme", "plan": "growth", "card": "[scrubbed]" }
404
+ }
405
+ },
406
+ {
407
+ "name": "v2 custom context: values pass the pattern scrub",
408
+ "event": {
409
+ "message": "boom",
410
+ "context": {
411
+ "cartTotal": "49.99",
412
+ "sessionToken": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJVadQssw5c"
413
+ }
414
+ },
415
+ "expected": {
416
+ "message": "boom",
417
+ "context": { "cartTotal": "49.99", "sessionToken": "[scrubbed]" }
418
+ }
419
+ },
420
+ {
421
+ "name": "v2 device: runtime metadata is left intact (it carries no customer data)",
422
+ "event": {
423
+ "message": "boom",
424
+ "device": { "runtime": "node", "runtimeVersion": "22.4.0", "platform": "linux", "arch": "arm64" }
425
+ },
426
+ "expected": {
427
+ "message": "boom",
428
+ "device": { "runtime": "node", "runtimeVersion": "22.4.0", "platform": "linux", "arch": "arm64" }
429
+ }
430
+ },
431
+ {
432
+ "name": "v2 route: the matched route template is pattern-scrubbed like the path",
433
+ "event": {
434
+ "message": "boom",
435
+ "request": {
436
+ "method": "POST",
437
+ "path": "/webhooks/sk_live_abcdefgh12345678/deliver",
438
+ "route": "/webhooks/sk_live_abcdefgh12345678/deliver",
439
+ "status": 500
440
+ }
441
+ },
442
+ "expected": {
443
+ "message": "boom",
444
+ "request": {
445
+ "method": "POST",
446
+ "path": "/webhooks/[scrubbed]/deliver",
447
+ "route": "/webhooks/[scrubbed]/deliver",
448
+ "status": 500
449
+ }
450
+ }
451
+ },
452
+ {
453
+ "name": "v2 route: a parameterised route template survives untouched (it is the grouping key)",
454
+ "event": {
455
+ "message": "boom",
456
+ "request": { "method": "GET", "path": "/orders/8842", "route": "/orders/:id", "status": 500 }
457
+ },
458
+ "expected": {
459
+ "message": "boom",
460
+ "request": { "method": "GET", "path": "/orders/8842", "route": "/orders/:id", "status": 500 }
461
+ }
462
+ },
463
+ {
464
+ "name": "v2 local variables: captured frame locals pass the pattern scrub",
465
+ "event": {
466
+ "message": "boom",
467
+ "frames": [
468
+ {
469
+ "file": "app/billing.py",
470
+ "function": "charge",
471
+ "line": 12,
472
+ "inApp": true,
473
+ "vars": { "attempt": "3", "card": "4242424242424242", "api_key": "sk_live_abcdefgh12345678" }
474
+ }
475
+ ]
476
+ },
477
+ "expected": {
478
+ "message": "boom",
479
+ "frames": [
480
+ {
481
+ "file": "app/billing.py",
482
+ "function": "charge",
483
+ "line": 12,
484
+ "inApp": true,
485
+ "vars": { "attempt": "3", "card": "[scrubbed]", "api_key": "[scrubbed]" }
486
+ }
487
+ ]
488
+ }
489
+ },
490
+ {
491
+ "name": "v2 adapter forwarding: a framework tag and a route ride together, neither leaking headers (REA-182)",
492
+ "event": {
493
+ "message": "TypeError: undefined is not a function",
494
+ "tags": { "framework": "express" },
495
+ "request": { "method": "GET", "path": "/orders/8842", "route": "/orders/:id", "status": 500 },
496
+ "device": { "runtime": "node", "runtimeVersion": "22.4.0" }
497
+ },
498
+ "expected": {
499
+ "message": "TypeError: undefined is not a function",
500
+ "tags": { "framework": "express" },
501
+ "request": { "method": "GET", "path": "/orders/8842", "route": "/orders/:id", "status": 500 },
502
+ "device": { "runtime": "node", "runtimeVersion": "22.4.0" }
503
+ }
504
+ }
505
+ ]
506
+ }
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: realuptime-errors
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - RealUptime
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-08-24 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Captures exceptions with scrub-by-default PII handling, bounded buffering
14
+ with honest drop counters, and framework integrations for Rails, Rack and Sidekiq.
15
+ Standard library only.
16
+ email:
17
+ - support@realuptime.io
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - LICENSE
23
+ - README.md
24
+ - lib/realuptime/errors.rb
25
+ - lib/realuptime/errors/rack.rb
26
+ - lib/realuptime/errors/rails.rb
27
+ - lib/realuptime/errors/scrub.rb
28
+ - lib/realuptime/errors/sidekiq.rb
29
+ - lib/realuptime/errors/transport.rb
30
+ - lib/realuptime/errors/version.rb
31
+ - scrub-vectors.json
32
+ homepage: https://realuptime.io/error-tracking
33
+ licenses:
34
+ - MIT
35
+ metadata:
36
+ homepage_uri: https://realuptime.io/error-tracking
37
+ source_code_uri: https://github.com/RealUptimeHQ/realuptime-errors-ruby
38
+ bug_tracker_uri: https://github.com/RealUptimeHQ/realuptime-errors-ruby/issues
39
+ rubygems_mfa_required: 'true'
40
+ post_install_message:
41
+ rdoc_options: []
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '2.6'
49
+ required_rubygems_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ requirements: []
55
+ rubygems_version: 3.0.3.1
56
+ signing_key:
57
+ specification_version: 4
58
+ summary: Zero-dependency error tracking SDK for Ruby, Rails, Rack and Sidekiq (RealUptime
59
+ Errors).
60
+ test_files: []