@rustrak/client 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,200 +1,180 @@
1
- # @rustrak/client
2
-
3
- TypeScript client for the Rustrak error tracking API. Provides a type-safe, fully-featured interface for interacting with Rustrak's REST API.
4
-
5
- ## Features
6
-
7
- - **Type-Safe**: Full TypeScript support with runtime validation using Zod
8
- - **Lightweight**: ~28KB total bundle size (ky 3KB + zod 10KB + client 15KB)
9
- - **Automatic Retry**: Built-in retry logic for transient failures
10
- - **Error Handling**: Structured error classes for different failure scenarios
11
- - **Pagination**: First-class support for cursor-based pagination
1
+ <div align="center">
2
+ <a href="https://abians.github.io/rustrak">
3
+ <img src="https://raw.githubusercontent.com/AbianS/rustrak/main/apps/docs/public/logo.svg" alt="Rustrak" width="64" height="64" />
4
+ </a>
5
+ <h1>@rustrak/client</h1>
6
+ <p>Official TypeScript client for the <a href="https://abians.github.io/rustrak">Rustrak</a> self-hosted error tracking API</p>
7
+
8
+ <p>
9
+ <a href="https://www.npmjs.com/package/@rustrak/client">
10
+ <img src="https://img.shields.io/npm/v/@rustrak/client?style=flat-square&color=cb3837" alt="npm version" />
11
+ </a>
12
+ <a href="https://www.npmjs.com/package/@rustrak/client">
13
+ <img src="https://img.shields.io/npm/dw/@rustrak/client?style=flat-square" alt="weekly downloads" />
14
+ </a>
15
+ <a href="https://bundlephobia.com/package/@rustrak/client">
16
+ <img src="https://img.shields.io/bundlephobia/minzip/@rustrak/client?style=flat-square&label=bundle" alt="bundle size" />
17
+ </a>
18
+ <a href="https://github.com/AbianS/rustrak/blob/main/LICENSE">
19
+ <img src="https://img.shields.io/npm/l/@rustrak/client?style=flat-square" alt="license" />
20
+ </a>
21
+ <a href="https://github.com/AbianS/rustrak/actions/workflows/ci.yml">
22
+ <img src="https://img.shields.io/github/actions/workflow/status/AbianS/rustrak/ci.yml?style=flat-square&label=CI" alt="CI" />
23
+ </a>
24
+ </p>
25
+
26
+ <p>
27
+ <a href="https://abians.github.io/rustrak/sdks/client">Documentation</a>
28
+ ·
29
+ <a href="https://github.com/AbianS/rustrak">GitHub</a>
30
+ ·
31
+ <a href="https://github.com/AbianS/rustrak/issues">Report a Bug</a>
32
+ </p>
33
+ </div>
34
+
35
+ ---
36
+
37
+ `@rustrak/client` is the official TypeScript client for [Rustrak](https://abians.github.io/rustrak) — an ultra-lightweight, self-hosted error tracking system compatible with any Sentry SDK. This package wraps the Rustrak REST API with full type safety, runtime validation via Zod, built-in retry logic, and structured error handling. Total bundle size: ~28 KB.
12
38
 
13
39
  ## Installation
14
40
 
15
41
  ```bash
42
+ npm install @rustrak/client
43
+ # or
16
44
  pnpm add @rustrak/client
45
+ # or
46
+ yarn add @rustrak/client
17
47
  ```
18
48
 
49
+ **Requirements**: Node.js ≥ 18, TypeScript ≥ 5
50
+
19
51
  ## Quick Start
20
52
 
21
53
  ```typescript
22
54
  import { RustrakClient } from '@rustrak/client';
23
55
 
24
56
  const client = new RustrakClient({
25
- baseUrl: 'http://localhost:8080',
26
- token: 'your-api-token',
57
+ baseUrl: 'https://your-rustrak-instance.example.com',
58
+ token: process.env.RUSTRAK_API_TOKEN!,
27
59
  });
28
60
 
29
61
  // List all projects
30
62
  const projects = await client.projects.list();
31
63
 
32
- // Get issues for a project
33
- const { items, next_cursor, has_more } = await client.issues.list(1);
64
+ // Paginate through open issues
65
+ const { items, next_cursor, has_more } = await client.issues.list(1, {
66
+ sort: 'last_seen',
67
+ order: 'desc',
68
+ });
34
69
 
35
- // Get events for an issue
36
- const events = await client.events.list(1, 'issue-uuid');
70
+ // Resolve an issue
71
+ await client.issues.updateState(1, 'issue-id', { is_resolved: true });
37
72
  ```
38
73
 
39
- ## Usage
74
+ **[Full documentation →](https://abians.github.io/rustrak/sdks/client)**
75
+
76
+ ## Features
77
+
78
+ - **Type-safe** — All API responses validated at runtime with Zod; types are inferred from schemas
79
+ - **Lightweight** — ~28 KB total (ky 3 KB + zod 10 KB + client 15 KB)
80
+ - **Automatic retry** — Exponential backoff on transient failures (408, 429, 5xx)
81
+ - **Structured errors** — Typed error classes for every HTTP status and failure mode
82
+ - **Cursor pagination** — First-class support for paginated responses across all list endpoints
83
+ - **97% test coverage** — 133 tests (unit + integration with MSW)
84
+
85
+ ## API Reference
40
86
 
41
87
  ### Configuration
42
88
 
43
89
  ```typescript
44
- import { RustrakClient } from '@rustrak/client';
45
-
46
90
  const client = new RustrakClient({
47
- baseUrl: 'https://rustrak.example.com',
48
- token: 'your-bearer-token',
49
- timeout: 30000, // Optional: request timeout in ms (default: 30000)
50
- maxRetries: 2, // Optional: max retry attempts (default: 2)
51
- headers: {
52
- // Optional: custom headers
53
- 'X-Custom-Header': 'value',
54
- },
91
+ baseUrl: 'https://rustrak.example.com', // required
92
+ token: 'your-bearer-token', // required
93
+ timeout: 30000, // optional, ms (default: 30000)
94
+ maxRetries: 2, // optional (default: 2)
95
+ headers: {}, // optional custom headers
55
96
  });
56
97
  ```
57
98
 
58
99
  ### Projects
59
100
 
60
101
  ```typescript
61
- // List all projects
62
102
  const projects = await client.projects.list();
63
-
64
- // Get a single project
65
- const project = await client.projects.get(1);
66
-
67
- // Create a project
68
- const newProject = await client.projects.create({
69
- name: 'My App',
70
- slug: 'my-app', // Optional
71
- });
72
-
73
- // Update a project
74
- const updated = await client.projects.update(1, {
75
- name: 'Updated Name',
76
- });
77
-
78
- // Delete a project
103
+ const project = await client.projects.get(1);
104
+ const created = await client.projects.create({ name: 'My App', slug: 'my-app' });
105
+ const updated = await client.projects.update(1, { name: 'New Name' });
79
106
  await client.projects.delete(1);
80
107
  ```
81
108
 
82
109
  ### Issues
83
110
 
84
111
  ```typescript
85
- // List issues with pagination
86
- const response = await client.issues.list(projectId, {
87
- sort: 'last_seen', // 'digest_order' | 'last_seen'
88
- order: 'desc', // 'asc' | 'desc'
112
+ // List with filters and cursor pagination
113
+ const { items, next_cursor, has_more } = await client.issues.list(projectId, {
114
+ sort: 'last_seen', // 'digest_order' | 'last_seen'
115
+ order: 'desc', // 'asc' | 'desc'
89
116
  include_resolved: false,
90
- cursor: 'eyJzb3J0...', // Optional: pagination cursor
117
+ cursor: 'eyJzb3J0...', // from previous response
91
118
  });
92
119
 
93
- // Paginate through all issues
94
- let cursor: string | undefined;
95
- do {
96
- const { items, next_cursor, has_more } = await client.issues.list(projectId, {
97
- cursor,
98
- });
99
-
100
- // Process items...
101
- cursor = next_cursor;
102
- } while (cursor);
103
-
104
- // Get a single issue
105
120
  const issue = await client.issues.get(projectId, issueId);
106
-
107
- // Update issue state
108
- const resolved = await client.issues.updateState(projectId, issueId, {
109
- is_resolved: true,
110
- is_muted: false,
111
- });
112
-
113
- // Delete an issue
121
+ await client.issues.updateState(projectId, issueId, { is_resolved: true });
114
122
  await client.issues.delete(projectId, issueId);
115
123
  ```
116
124
 
117
125
  ### Events
118
126
 
119
127
  ```typescript
120
- // List events for an issue
121
- const { items, next_cursor } = await client.events.list(projectId, issueId, {
122
- order: 'desc',
123
- cursor: 'optional-cursor',
124
- });
125
-
126
- // Get event details
127
- const event = await client.events.get(projectId, issueId, eventId);
128
-
129
- // Access full Sentry event data
130
- console.log(event.data); // Full JSON payload
128
+ const { items } = await client.events.list(projectId, issueId, { order: 'desc' });
129
+ const event = await client.events.get(projectId, issueId, eventId);
130
+ console.log(event.data); // Full Sentry event payload
131
131
  ```
132
132
 
133
133
  ### Auth Tokens
134
134
 
135
135
  ```typescript
136
- // List tokens (masked)
137
- const tokens = await client.tokens.list();
138
-
139
- // Get a single token (masked)
140
- const token = await client.tokens.get(1);
141
-
142
- // Create a token (full token only shown once!)
143
- const created = await client.tokens.create({
144
- description: 'CI/CD token',
145
- });
146
- console.log(created.token); // Save this! Won't be shown again
147
-
148
- // Delete a token
136
+ const tokens = await client.tokens.list();
137
+ const created = await client.tokens.create({ description: 'CI token' });
138
+ console.log(created.token); // Save this — shown only once
149
139
  await client.tokens.delete(1);
150
140
  ```
151
141
 
152
142
  ## Error Handling
153
143
 
154
- The client throws structured error classes for different scenarios:
155
-
156
144
  ```typescript
157
145
  import {
158
- RustrakError,
159
- NetworkError,
160
146
  AuthenticationError,
161
- RateLimitError,
147
+ AuthorizationError,
162
148
  NotFoundError,
149
+ RateLimitError,
150
+ NetworkError,
151
+ ServerError,
163
152
  ValidationError,
164
153
  } from '@rustrak/client';
165
154
 
166
155
  try {
167
156
  await client.projects.list();
168
157
  } catch (error) {
169
- if (error instanceof RateLimitError) {
170
- console.log(`Rate limited. Retry after ${error.retryAfter}s`);
171
- } else if (error instanceof AuthenticationError) {
172
- console.log('Invalid token');
173
- } else if (error instanceof NotFoundError) {
174
- console.log('Resource not found');
175
- } else if (error instanceof NetworkError) {
176
- console.log('Network error - will retry automatically');
177
- } else if (error instanceof ValidationError) {
178
- console.log('API response validation failed');
179
- console.log(error.getValidationDetails());
180
- }
158
+ if (error instanceof RateLimitError) console.log(`Retry after ${error.retryAfter}s`);
159
+ else if (error instanceof AuthenticationError) redirect('/login');
160
+ else if (error instanceof NotFoundError) console.log('Not found');
161
+ else if (error instanceof NetworkError) console.log('Will retry automatically');
162
+ else if (error instanceof ValidationError) console.log(error.getValidationDetails());
181
163
  }
182
164
  ```
183
165
 
184
- ### Error Types
166
+ | Error Class | HTTP | Retryable |
167
+ |---|---|---|
168
+ | `NetworkError` | — | ✅ |
169
+ | `AuthenticationError` | 401 | ❌ |
170
+ | `AuthorizationError` | 403 | ❌ |
171
+ | `NotFoundError` | 404 | ❌ |
172
+ | `BadRequestError` | 400 | ❌ |
173
+ | `RateLimitError` | 429 | ✅ |
174
+ | `ServerError` | 500+ | ✅ |
175
+ | `ValidationError` | — | ❌ |
185
176
 
186
- | Error Class | HTTP Status | Retryable | Use Case |
187
- |-------------|-------------|-----------|----------|
188
- | `NetworkError` | - | ✅ | Connection issues, timeouts |
189
- | `AuthenticationError` | 401 | ❌ | Invalid credentials |
190
- | `AuthorizationError` | 403 | ❌ | Insufficient permissions |
191
- | `NotFoundError` | 404 | ❌ | Resource doesn't exist |
192
- | `BadRequestError` | 400 | ❌ | Invalid request payload |
193
- | `RateLimitError` | 429 | ✅ | Rate limit exceeded |
194
- | `ServerError` | 500+ | ✅ | Server-side errors |
195
- | `ValidationError` | - | ❌ | Response schema mismatch |
196
-
197
- ## Usage with Next.js
177
+ ## Next.js Integration
198
178
 
199
179
  ### Server Component
200
180
 
@@ -206,48 +186,8 @@ export default async function ProjectsPage() {
206
186
  baseUrl: process.env.RUSTRAK_API_URL!,
207
187
  token: process.env.RUSTRAK_API_TOKEN!,
208
188
  });
209
-
210
189
  const projects = await client.projects.list();
211
-
212
- return (
213
- <div>
214
- {projects.map((project) => (
215
- <div key={project.id}>{project.name}</div>
216
- ))}
217
- </div>
218
- );
219
- }
220
- ```
221
-
222
- ### Client Component with SWR
223
-
224
- ```typescript
225
- 'use client';
226
-
227
- import useSWR from 'swr';
228
- import { RustrakClient } from '@rustrak/client';
229
-
230
- const client = new RustrakClient({
231
- baseUrl: process.env.NEXT_PUBLIC_RUSTRAK_API_URL!,
232
- token: process.env.NEXT_PUBLIC_RUSTRAK_API_TOKEN!,
233
- });
234
-
235
- export function IssuesList({ projectId }: { projectId: number }) {
236
- const { data, error, isLoading } = useSWR(
237
- ['issues', projectId],
238
- () => client.issues.list(projectId)
239
- );
240
-
241
- if (error) return <div>Error: {error.message}</div>;
242
- if (isLoading) return <div>Loading...</div>;
243
-
244
- return (
245
- <div>
246
- {data?.items.map((issue) => (
247
- <div key={issue.id}>{issue.title}</div>
248
- ))}
249
- </div>
250
- );
190
+ return <ProjectsList projects={projects} />;
251
191
  }
252
192
  ```
253
193
 
@@ -255,7 +195,6 @@ export function IssuesList({ projectId }: { projectId: number }) {
255
195
 
256
196
  ```typescript
257
197
  'use server';
258
-
259
198
  import { RustrakClient } from '@rustrak/client';
260
199
 
261
200
  export async function resolveIssue(projectId: number, issueId: string) {
@@ -263,16 +202,13 @@ export async function resolveIssue(projectId: number, issueId: string) {
263
202
  baseUrl: process.env.RUSTRAK_API_URL!,
264
203
  token: process.env.RUSTRAK_API_TOKEN!,
265
204
  });
266
-
267
- return await client.issues.updateState(projectId, issueId, {
268
- is_resolved: true,
269
- });
205
+ return client.issues.updateState(projectId, issueId, { is_resolved: true });
270
206
  }
271
207
  ```
272
208
 
273
209
  ## TypeScript
274
210
 
275
- The client is written in TypeScript with strict mode enabled. All types are exported:
211
+ All types are exported and inferred from Zod schemas single source of truth:
276
212
 
277
213
  ```typescript
278
214
  import type {
@@ -280,37 +216,28 @@ import type {
280
216
  Issue,
281
217
  Event,
282
218
  EventDetail,
219
+ AuthToken,
283
220
  PaginatedResponse,
284
221
  CreateProject,
285
222
  UpdateIssueState,
286
223
  } from '@rustrak/client';
287
-
288
- const project: Project = await client.projects.get(1);
289
- const issues: PaginatedResponse<Issue> = await client.issues.list(1);
290
224
  ```
291
225
 
292
- ## Development
226
+ ## Related Packages
293
227
 
294
- ```bash
295
- # Install dependencies
296
- pnpm install
228
+ | Package | Description |
229
+ |---|---|
230
+ | [`@rustrak/mcp`](https://www.npmjs.com/package/@rustrak/mcp) | MCP server — gives Claude Desktop, Cursor, and Continue direct access to your Rustrak instance via 18 tools |
297
231
 
298
- # Build
299
- pnpm build
232
+ ## What is Rustrak?
300
233
 
301
- # Type check
302
- pnpm check-types
234
+ [Rustrak](https://abians.github.io/rustrak) is a self-hosted error tracking server written in Rust that is fully compatible with any Sentry SDK. Drop-in replacement for Sentry — no code changes needed. Runs on ~50 MB of memory as a single binary or Docker image.
303
235
 
304
- # Run tests
305
- pnpm test
306
-
307
- # Run tests in watch mode
308
- pnpm test:watch
309
-
310
- # Generate coverage
311
- pnpm test:coverage
312
- ```
236
+ - [Getting Started](https://abians.github.io/rustrak/getting-started/overview)
237
+ - [Self-Hosting Guide](https://abians.github.io/rustrak/configuration/production)
238
+ - [API Reference](https://abians.github.io/rustrak/api-reference)
239
+ - [GitHub](https://github.com/AbianS/rustrak)
313
240
 
314
241
  ## License
315
242
 
316
- GPL-3.0
243
+ [GPL-3.0](https://github.com/AbianS/rustrak/blob/main/LICENSE)
package/dist/index.cjs CHANGED
@@ -477,6 +477,30 @@ var createProjectSchema = zod.z.object({
477
477
  var updateProjectSchema = zod.z.object({
478
478
  name: zod.z.string().min(1).optional()
479
479
  });
480
+ var chunkUploadCapabilitySchema = zod.z.object({
481
+ url: zod.z.string(),
482
+ chunkSize: zod.z.number().int(),
483
+ chunksPerRequest: zod.z.number().int(),
484
+ maxRequestSize: zod.z.number().int(),
485
+ hashAlgorithm: zod.z.string(),
486
+ accept: zod.z.array(zod.z.string()),
487
+ concurrency: zod.z.number().int().optional()
488
+ });
489
+ var assembleResponseSchema = zod.z.object({
490
+ state: zod.z.string(),
491
+ missingChunks: zod.z.array(zod.z.string()),
492
+ detail: zod.z.string().optional()
493
+ });
494
+ var sourceMapFileSchema = zod.z.object({
495
+ debugId: zod.z.string(),
496
+ fileType: zod.z.string(),
497
+ size: zod.z.number().int(),
498
+ timesUsed: zod.z.number().int(),
499
+ dateUploaded: zod.z.string()
500
+ });
501
+ var listSourceMapsResponseSchema = zod.z.object({
502
+ data: zod.z.array(sourceMapFileSchema)
503
+ });
480
504
  var authTokenSchema = zod.z.object({
481
505
  id: zod.z.number().int(),
482
506
  token_prefix: zod.z.string(),
@@ -623,6 +647,58 @@ var ProjectsResource = class extends BaseResource {
623
647
  }
624
648
  };
625
649
 
650
+ // src/resources/sourcemaps.ts
651
+ var SourceMapsResource = class extends BaseResource {
652
+ /**
653
+ * Get chunk upload capabilities for an organization.
654
+ * Returns the upload URL, chunk size limits, and accepted artifact types.
655
+ */
656
+ async getChunkUploadCapability(orgSlug) {
657
+ const data = await this.http.get(`api/0/organizations/${orgSlug}/chunk-upload/`).json();
658
+ return this.validate(data, chunkUploadCapabilitySchema);
659
+ }
660
+ /**
661
+ * Upload chunks as multipart/form-data, batching by `chunksPerRequest` (default 64).
662
+ * Each part's field name must be the pre-computed SHA-1 hash of its content — the server
663
+ * rejects mismatches with 400. Callers must supply the hash (also needed for assembleBundle).
664
+ */
665
+ async uploadChunks(orgSlug, chunks, chunksPerRequest = 64) {
666
+ if (!Number.isInteger(chunksPerRequest) || chunksPerRequest <= 0) {
667
+ throw new Error("chunksPerRequest must be a positive integer");
668
+ }
669
+ for (let i = 0; i < chunks.length; i += chunksPerRequest) {
670
+ const batch = chunks.slice(i, i + chunksPerRequest);
671
+ const form = new FormData();
672
+ for (const chunk of batch) {
673
+ form.append(chunk.hash, chunk.data);
674
+ }
675
+ await this.http.post(`api/0/organizations/${orgSlug}/chunk-upload/`, {
676
+ body: form
677
+ });
678
+ }
679
+ }
680
+ /**
681
+ * Trigger assembly of a previously uploaded artifact bundle.
682
+ *
683
+ * @returns The assembly state. Poll until `state === "ok"` or `state === "error"`.
684
+ * When `state === "not_found"` the `missingChunks` array lists what
685
+ * still needs uploading via `uploadChunks`.
686
+ */
687
+ async assembleBundle(orgSlug, input) {
688
+ const data = await this.http.post(`api/0/organizations/${orgSlug}/artifactbundle/assemble/`, {
689
+ json: input
690
+ }).json();
691
+ return this.validate(data, assembleResponseSchema);
692
+ }
693
+ /**
694
+ * List all source map files uploaded for a project.
695
+ */
696
+ async list(orgSlug, projectSlug) {
697
+ const data = await this.http.get(`api/0/projects/${orgSlug}/${projectSlug}/files/source-maps/`).json();
698
+ return this.validate(data, listSourceMapsResponseSchema);
699
+ }
700
+ };
701
+
626
702
  // src/resources/tokens.ts
627
703
  var TokensResource = class extends BaseResource {
628
704
  /**
@@ -750,6 +826,10 @@ var RustrakClient = class {
750
826
  * Alert Rules API resource (per-project alert configuration)
751
827
  */
752
828
  alertRules;
829
+ /**
830
+ * Source Maps API resource (sentry-cli artifact bundle upload protocol)
831
+ */
832
+ sourceMaps;
753
833
  /**
754
834
  * Create a new Rustrak API client
755
835
  *
@@ -764,6 +844,7 @@ var RustrakClient = class {
764
844
  this.tokens = new TokensResource(this.http);
765
845
  this.alertChannels = new AlertChannelsResource(this.http);
766
846
  this.alertRules = new AlertRulesResource(this.http);
847
+ this.sourceMaps = new SourceMapsResource(this.http);
767
848
  }
768
849
  };
769
850