Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
e49318a
docs: update README with symbolication feature
alltheseas Jan 16, 2026
7bdc7dd
docs: simplify README relay limits and cleanup
alltheseas Jan 16, 2026
8b04dc8
docs(rust): add symbolication section with proper code block tags
alltheseas Jan 16, 2026
fb8de5c
feat(rust): add CHK chunking for large crash reports (Phase 2)
alltheseas Jan 16, 2026
8c39004
feat(rust): implement chunk fetching for large crash reports
alltheseas Jan 16, 2026
529ba43
docs(rust): expand rustdoc for fetch_chunks function
alltheseas Jan 16, 2026
2f9fe6b
feat(receiver): cross-relay chunk aggregation
alltheseas Jan 16, 2026
68ef61d
feat(electron): add CHK chunking for large crash reports
alltheseas Jan 16, 2026
1ae539b
feat(react-native): add CHK chunking for large crash reports
alltheseas Jan 16, 2026
eab5fa0
feat(go): add CHK chunking for large crash reports
alltheseas Jan 16, 2026
6b73a8d
feat(python): add CHK chunking for large crash reports
alltheseas Jan 16, 2026
beabb3b
feat(android,dart): add CHK chunking modules for large crash reports
alltheseas Jan 16, 2026
c052d94
feat(android,dart): integrate chunking into send flow
alltheseas Jan 16, 2026
ff356ca
feat(sdk): add 100ms delay between chunk publications
alltheseas Jan 16, 2026
ed888cd
chore(electron,react-native): update package-lock.json
alltheseas Jan 16, 2026
130d154
feat(dart,android): add relay rate limiting and progress callbacks
alltheseas Jan 16, 2026
22bb93c
feat(react-native): add relay rate limiting and progress callbacks
alltheseas Jan 16, 2026
2076b2a
feat(go,python): add relay rate limiting and progress callbacks
alltheseas Jan 16, 2026
bf8d592
feat(rust): add relay hints support for chunk fetching
alltheseas Jan 16, 2026
abb6492
feat(sdk): add publish verification and retry for chunk uploads
alltheseas Jan 16, 2026
a462bb9
fix(sdk): increase verification delay to 500ms
alltheseas Jan 16, 2026
d261088
docs: add reliability analysis for chunk uploads
alltheseas Jan 16, 2026
643375b
fix(sdk): align CHK encryption with hashtree-core across all SDKs
alltheseas Jan 16, 2026
1d94ed1
fix(rust): prevent panic on ws:// relay URLs in subscription ID
alltheseas Jan 16, 2026
2559d04
test: add CHK encryption cross-SDK test vectors
alltheseas Jan 16, 2026
6b9070d
chore: add .beads/ to gitignore
alltheseas Jan 16, 2026
f33ed4c
fix: address code review findings across all SDKs
alltheseas Jan 16, 2026
bee0184
fix: improve chunk upload error handling and fix Android pubKey deriv…
alltheseas Jan 16, 2026
f1991bb
fix: NIP-17/NIP-59 compliance fixes across all SDKs
alltheseas Jan 16, 2026
7bb5ed5
fix: add remaining NIP-17/NIP-59 receiver validations
alltheseas Jan 16, 2026
5544559
feat: fingerprint-based crash grouping (Rollbar-style algorithm)
alltheseas Feb 24, 2026
accd283
feat: dashboard groups view uses fingerprint titles and drill-down
alltheseas Feb 24, 2026
e75ca0a
docs: add fingerprint grouping to changelog
alltheseas Feb 24, 2026
da940f1
docs: upgrade AGENTS.md with crash reporting best practices
alltheseas Feb 24, 2026
43be008
docs: rewrite README with privacy-first positioning for indie/team devs
alltheseas Feb 24, 2026
8390620
docs: add invariant comments and conformance tests to storage/web
alltheseas Feb 24, 2026
98f062b
chore: add pre-commit verification script for Rust crate
alltheseas Feb 24, 2026
e9f1a6b
test: add SDK conformance test vectors and payload schema
alltheseas Feb 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,6 @@ node_modules/
# Test vectors
test-vectors/node_modules/
test-vectors/package-lock.json

# Beads (local issue tracking)
.beads/
265 changes: 240 additions & 25 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

This document provides guidelines for AI agents and human contributors working on BugStr.

## Before Committing Checklist

Every commit touching code must pass:

- [ ] `cargo test` (Rust) or equivalent per platform
- [ ] `cargo build --release` compiles without errors
- [ ] CHANGELOG.md updated for user-facing changes
- [ ] Public functions have docstrings
- [ ] No PII in test fixtures (no real emails, IPs, pubkeys)

## Project Overview

BugStr is a privacy-focused crash reporting library for Nostr applications. It uses NIP-17 gift-wrapped encrypted messages to deliver crash reports with user consent.
Expand All @@ -25,24 +35,161 @@ BugStr is a privacy-focused crash reporting library for Nostr applications. It u
- **NIP-59** - Gift Wrap (rumor → seal → gift wrap)
- **NIP-40** - Expiration Timestamp

## Privacy Requirements

Privacy is bugstr's core differentiator. All code must uphold these rules:

### PII Collection Defaults

- SDKs MUST NOT collect user-identifiable data by default
- No IP addresses, email addresses, usernames, or device IDs unless explicitly opted in
- Crash report content (stack traces, messages) must not contain PII from the user's app — this is the SDK integrator's responsibility, but SDKs should document the risk
- Test fixtures and examples must use synthetic data only

### User Consent

- Crash reporting MUST be opt-in, not opt-out
- SDKs must provide `setEnabled(bool)` or equivalent to control collection
- No data leaves the device before user consent
- Crashes occurring before consent can be cached locally and sent after consent is granted

### Data Handling

- All crash data is encrypted end-to-end via NIP-17 (gift wrap)
- The relay never sees plaintext crash data
- Receiver (developer) is the only party that can decrypt reports
- Local crash caches should be stored in app-private directories

## Crash Report Payload Schema

All SDKs must produce crash reports conforming to this JSON schema:

```json
{
"message": "Error description (string, required)",
"stack": "Full stack trace (string, optional)",
"environment": "production|staging|development (string, optional)",
"release": "1.0.0+42 (string, optional)",
"app_id": "com.example.myapp (string, optional)",
"platform": "android|flutter|electron|rust|go|python|react-native (string, optional)"
}
```

- `message`: The exception message or error description
- `stack`: Full stack trace as a single string with newlines
- Stack traces must use the platform's native format (Java `at` frames, Dart `#N` frames, JS `at` frames, etc.)
- Do NOT strip or truncate stack traces at the SDK level — the receiver handles grouping

### Transport Kinds

| Kind | Purpose | Payload |
|------|---------|---------|
| 10420 | Direct crash report | `DirectPayload` wrapping the JSON above |
| 10421 | Chunked manifest | `ManifestPayload` with chunk IDs |
| 10422 | Chunk | `ChunkPayload` with index + data |

Use kind 10420 for payloads under 40KB. Above that, chunk via kind 10421/10422.

Comment on lines +83 to +92
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n "DIRECT_SIZE_THRESHOLD|MAX_CHUNK_SIZE|chunk.*size|direct.*size|40KB|50KB|48KB|32KB" -S

Repository: alltheseas/bugstr

Length of output: 11433


🏁 Script executed:

cat -n AGENTS.md | head -100

Repository: alltheseas/bugstr

Length of output: 4320


🏁 Script executed:

cat -n AGENTS.md | sed -n '150,160p'

Repository: alltheseas/bugstr

Length of output: 489


Transport size thresholds in AGENTS.md are incorrect—update to match implementation.

This doc states direct payloads use 40KB and chunks use 32KB, but all SDKs (React Native, Electron, Python, Rust, Kotlin, Dart, Go, Android) implement 50KB direct threshold and 48KB chunk size. The CHANGELOG also confirms 50KB. Update lines 91, 154–155 to align with the actual constants:

  • Change "40KB" → "50KB"
  • Change "32KB" → "48KB"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@AGENTS.md` around lines 83 - 92, Update the Transport Kinds documentation in
the "Transport Kinds" section to match the SDK implementations by replacing the
incorrect size thresholds: change the displayed direct payload threshold string
"40KB" to "50KB" and change the chunk size string "32KB" to "48KB" (these appear
in the table/passage describing use of kind 10420 vs 10421/10422); ensure the
textual guidance about when to use direct vs chunked payloads reflects 50KB
direct / 48KB chunk sizes so it matches the SDK constants and CHANGELOG.

## Crash Grouping (Fingerprint Algorithm)

The Rust receiver groups crashes using a Rollbar-style fingerprint. SDKs do not need to compute fingerprints — this is server-side. But understanding the algorithm helps when debugging grouping issues.

### Algorithm

```
input = exception_type + "\n"
for each line in stack_trace:
if is_stack_frame(line) and is_in_app(frame):
input += normalized_filename + ":" + method_name + "\n"
if no in-app frames found:
input += normalize_message(message) # strip hex, IPs, timestamps, large numbers
fingerprint = "v1:" + hex(sha256(input))[..32]
```
Comment on lines +99 to +107
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add language identifiers to fenced code blocks (MD040).
Both algorithm blocks are missing a language tag. Adding a hint (e.g., text) will satisfy markdownlint and improve readability.

🔧 Suggested fix
-```
+```text
 input = exception_type + "\n"
 for each line in stack_trace:
     if is_stack_frame(line) and is_in_app(frame):
         input += normalized_filename + ":" + method_name + "\n"
 if no in-app frames found:
     input += normalize_message(message)    # strip hex, IPs, timestamps, large numbers
 fingerprint = "v1:" + hex(sha256(input))[..32]
-```
+```

 ...

-```
+```text
 1. content_hash = SHA256(plaintext)
 2. key = HKDF-SHA256(
      ikm: content_hash,
      salt: "hashtree-chk",
      info: "encryption-key",
      length: 32
    )
 3. ciphertext = AES-256-GCM(
      key: key,
      nonce: 12 zero bytes,
      plaintext: data
    )
 4. output = [ciphertext][16-byte auth tag]
-```
+```

Also applies to: 280-294

🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 99-99: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@AGENTS.md` around lines 99 - 107, The fenced code blocks in AGENTS.md are
missing language identifiers causing markdownlint MD040 failures; update the
opening backticks for the algorithm block that begins with `input =
exception_type + "\n"` and the numbered block that begins with `1. content_hash
= SHA256(plaintext)` (and the similar block at lines 280-294) to include a
language hint (e.g., add `text` immediately after the triple backticks) so each
code fence reads ```text and closes as ``` ensuring all fenced blocks have
language identifiers.


### What makes frames "in-app"

Excluded (framework/runtime) frames:
- `dart:async/*`, `dart:core/*`, `dart:io/*`
- `flutter/*`, `packages/flutter/*`
- `java.lang.*`, `java.util.*`, `android.*`, `androidx.*`, `dalvik.*`, `kotlin.*`
- `node:*`, `internal/*`
- `<anonymous>`, `native`, `Unknown Source`

### What gets stripped

- **Line numbers** — they change with unrelated edits
- **Frame indices** — `#0`, `#1`, etc.
- **Memory addresses** — `0x7fff...`
- In message fallback: hex values, IPs, timestamps, numbers > 5 digits

### Group titles

Human-readable titles are computed as: `"ExceptionType in method (file)"` using the first in-app frame. Falls back to `"ExceptionType: first line of message"`.

## SDK Design Contract

Every platform SDK must implement these capabilities:

### Required

1. **Opt-in consent** — No data sent without explicit `enable()` call
2. **Uncaught exception handler** — Hook into the platform's crash mechanism
3. **Offline caching** — Cache reports locally when network is unavailable; send on next launch
4. **Background sending** — Never block the main/UI thread for crash transmission
5. **NIP-17 gift wrap** — Encrypt via NIP-44, wrap per NIP-59, send to configured relay(s)
6. **Payload compression** — Gzip payloads > 1KB before wrapping (see `compression.rs`)
7. **Chunking** — Payloads > 40KB must be chunked (kinds 10421/10422)

### Recommended

8. **`beforeSend` callback** — Let integrators inspect/modify/drop reports before transmission
9. **Breadcrumbs** — Record a trail of events (navigation, HTTP, UI) leading up to the crash (max 100)
10. **Context/tags** — Allow setting key-value metadata (app version, OS version, device model)
11. **Rate limiting** — Max 10 reports per minute per SDK instance to prevent flood
12. **Non-fatal reporting** — `reportError(exception)` for caught exceptions

### Payload Limits

- Maximum crash content: **500KB** before compression
- Maximum compressed payload for direct send: **40KB**
- Above 40KB: chunk into 32KB pieces via kind 10421/10422
- Maximum breadcrumbs per report: **100 entries**

## Symbolication

The Rust receiver supports server-side symbolication. SDKs must tag reports for symbol lookup:

### Required metadata for symbolication

- `platform`: Identifies which symbolicator to use
- `app_id`: Package name / bundle ID (e.g., `com.example.myapp`)
- `release`: Version string (e.g., `1.0.0+42`)

### Mapping file upload

Mapping files are stored in: `<mappings_dir>/<platform>/<app_id>/<version>/<file>`

| Platform | File Type | Upload Tool |
|----------|-----------|-------------|
| Android | `mapping.txt` (ProGuard/R8) | CI upload or manual |
| Flutter/Dart | `.symbols` | CI upload |
| JavaScript/Electron | `.map` (source maps) | CI upload |
| React Native | `.map` (Hermes + source maps) | CI upload |

## Coding Requirements

### 1. Documentation

Ensure docstring coverage for any code added or modified:
All public functions must have docstrings in the platform's standard format:

- **Kotlin**: Use KDoc format (`/** ... */`)
- **Dart**: Use dartdoc format (`/// ...`)
- **TypeScript/Electron**: Use JSDoc format (`/** ... */`)
- **Rust**: Use rustdoc format (`/// ...` or `//!`)
- **Go**: Use godoc format (comment before declaration)
- **Python**: Use docstrings (`"""..."""`)
- **Kotlin**: KDoc (`/** ... */`)
- **Dart**: dartdoc (`/// ...`)
- **TypeScript/Electron**: JSDoc (`/** ... */`)
- **Rust**: rustdoc (`/// ...` or `//!`)
- **Go**: godoc (comment before declaration)
- **Python**: docstrings (`"""..."""`)

All public classes, methods, and non-trivial functions must have documentation explaining:
- Purpose and behavior
- Parameters and return values
- Exceptions that may be thrown
- Usage examples for complex APIs
Document: purpose, parameters, return values, thrown exceptions.

### 2. Commit Guidelines

Expand All @@ -60,13 +207,6 @@ Commits must be independently removable:
- Each commit should compile and pass tests
- Avoid tight coupling between commits in a PR

#### Human Readable Code

All code must be reviewable by human developers:
- Clear, descriptive variable and function names
- Appropriate comments for non-obvious logic
- Consistent formatting per language conventions

#### Cherry-Pick for Attribution

When incorporating work from other branches or contributors:
Expand Down Expand Up @@ -99,8 +239,7 @@ All user-facing changes require a CHANGELOG.md entry:
<optional body explaining what and why>

<optional footer>
Signed-off-by: name <email>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
```

Types: `feat`, `fix`, `docs`, `refactor`, `test`, `chore`
Expand All @@ -125,13 +264,83 @@ id = sha256(json([0, pubkey, created_at, kind, tags, content]))

Returns lowercase hex string (64 characters).

## Lessons Learned

### CHK Encryption Compatibility (Critical)

**Problem**: All SDKs implemented CHK (Content Hash Key) encryption differently from the Rust reference implementation, causing complete decryption failure.

**Root Cause**: Each SDK used its own interpretation of "encrypt with content hash":
- Some used AES-256-CBC with random IV
- Others omitted HKDF key derivation
- Ciphertext format varied (IV position, tag handling)

**The Correct Algorithm** (must match `hashtree-core` exactly):

```
1. content_hash = SHA256(plaintext)
2. key = HKDF-SHA256(
ikm: content_hash,
salt: "hashtree-chk",
info: "encryption-key",
length: 32
)
3. ciphertext = AES-256-GCM(
key: key,
nonce: 12 zero bytes,
plaintext: data
)
4. output = [ciphertext][16-byte auth tag]
```

**Why each component matters**:

| Component | Purpose | If Wrong |
|-----------|---------|----------|
| HKDF | Derives encryption key from content hash | Key mismatch → decryption fails |
| Salt `"hashtree-chk"` | Domain separation | Different key → decryption fails |
| Info `"encryption-key"` | Key purpose binding | Different key → decryption fails |
| Zero nonce | Safe for CHK (same key = same content) | Different ciphertext → verification fails |
| AES-GCM | Authenticated encryption | Different algorithm → decryption fails |

**Why zero nonce is safe**: CHK is convergent encryption - the same plaintext always produces the same key. Since the key is deterministic, using a random nonce would make ciphertext non-deterministic, breaking content-addressable storage. Zero nonce is safe because the key is never reused with different content.

**Verification checklist for new implementations**:
1. Generate test vector in Rust: `cargo test chunking -- --nocapture`
2. Encrypt same plaintext in your SDK
3. Compare: content hash, derived key, ciphertext must be byte-identical
4. Decrypt Rust ciphertext in your SDK (and vice versa)

**Platform-specific libraries**:

| Platform | HKDF | AES-GCM |
|----------|------|---------|
| Rust | `hashtree-core` | (built-in) |
| Dart | `pointycastle` HKDFKeyDerivator | `pointycastle` GCMBlockCipher |
| Kotlin | Manual HMAC-SHA256 | `javax.crypto` AES/GCM/NoPadding |
| Go | `golang.org/x/crypto/hkdf` | `crypto/cipher` NewGCM |
| Python | `cryptography` HKDF | `cryptography` AESGCM |
| TypeScript (Node) | `crypto` hkdfSync | `crypto` aes-256-gcm |
| TypeScript (RN) | `@noble/hashes/hkdf` | `@noble/ciphers/aes` gcm |

## Testing

### Unit Tests

- All new code should have corresponding unit tests
- Test edge cases and error conditions
- Mock external dependencies
- All new code must have corresponding unit tests
- Test edge cases: empty strings, null fields, malformed input
- Test interop: Rust receiver must handle all SDK payload formats
- Mock external dependencies (relays, network)

### Critical Test Scenarios

- Same exception + different stack = different fingerprints
- Same stack + different line numbers = same fingerprint
- URL-only content → `is_crash = false`, excluded from groups
- Dart, Java, JS frame parsing produces correct (method, file) pairs
- Gift wrap round-trip: encrypt in SDK → decrypt in Rust receiver
- Chunking round-trip: chunk → reassemble → decompress → original payload
- Compression: payloads > 1KB are compressed, < 1KB are sent raw

### Interoperability Testing

Expand All @@ -157,8 +366,14 @@ bugstr/
│ ├── lib/src/
│ ├── CHANGELOG.md
│ └── README.md
├── rust/ # Rust CLI + library
├── rust/ # Rust CLI + receiver + web dashboard
│ ├── src/
│ │ ├── storage.rs # SQLite storage, fingerprinting, grouping
│ │ ├── web.rs # REST API + embedded dashboard
│ │ ├── symbolication/ # Server-side symbolication
│ │ ├── chunking.rs # Payload chunking (CHK)
│ │ └── compression.rs # Gzip compression
│ ├── static/index.html # Dashboard frontend
│ ├── CHANGELOG.md
│ └── README.md
├── go/ # Go library
Expand Down
Loading