@rustrak/client 0.13.0 → 0.14.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 +123 -37
- package/dist/index.cjs +850 -417
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +195 -144
- package/dist/index.d.ts +195 -144
- package/dist/index.js +841 -409
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -58,17 +58,24 @@ const client = new RustrakClient({
|
|
|
58
58
|
token: process.env.RUSTRAK_API_TOKEN!,
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
-
//
|
|
61
|
+
// Every method returns a Result. Nothing throws for an expected failure.
|
|
62
62
|
const projects = await client.projects.list();
|
|
63
|
+
if (!projects.success) {
|
|
64
|
+
console.error(projects.error.kind, projects.error.message);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
console.log(projects.data.items);
|
|
63
68
|
|
|
64
69
|
// Paginate through open issues
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
70
|
+
const issues = await client.issues.list(1, { sort: 'last_seen', order: 'desc' });
|
|
71
|
+
if (issues.success) {
|
|
72
|
+
const { items, next_cursor, has_more } = issues.data;
|
|
73
|
+
}
|
|
69
74
|
|
|
70
75
|
// Resolve an issue
|
|
71
|
-
await client.issues.updateState(1, 'issue-id', {
|
|
76
|
+
const resolved = await client.issues.updateState(1, 'issue-id', {
|
|
77
|
+
is_resolved: true,
|
|
78
|
+
});
|
|
72
79
|
```
|
|
73
80
|
|
|
74
81
|
**[Full documentation →](https://rustrak.github.io/rustrak/sdks/client)**
|
|
@@ -78,9 +85,10 @@ await client.issues.updateState(1, 'issue-id', { is_resolved: true });
|
|
|
78
85
|
- **Type-safe** — All API responses validated at runtime with Zod; types are inferred from schemas
|
|
79
86
|
- **Lightweight** — ~28 KB total (ky 3 KB + zod 10 KB + client 15 KB)
|
|
80
87
|
- **Automatic retry** — Exponential backoff on transient failures (408, 429, 5xx)
|
|
81
|
-
- **
|
|
88
|
+
- **No exceptions**: every method returns `Result<T, RustrakError>`, so failure is in the type, and the value survives the React server/client boundary
|
|
82
89
|
- **Cursor pagination** — First-class support for paginated responses across all list endpoints
|
|
83
|
-
- **
|
|
90
|
+
- **Redacted 5xx**: a server error never carries a server-supplied message, so internal detail cannot reach a UI
|
|
91
|
+
- **High test coverage**: 430+ tests (unit + integration with MSW)
|
|
84
92
|
|
|
85
93
|
## API Reference
|
|
86
94
|
|
|
@@ -135,44 +143,108 @@ console.log(event.data); // Full Sentry event payload
|
|
|
135
143
|
```typescript
|
|
136
144
|
const tokens = await client.tokens.list();
|
|
137
145
|
const created = await client.tokens.create({ description: 'CI token' });
|
|
138
|
-
|
|
146
|
+
if (created.success) {
|
|
147
|
+
console.log(created.data.token); // Save this, shown only once
|
|
148
|
+
}
|
|
139
149
|
await client.tokens.delete(1);
|
|
140
150
|
```
|
|
141
151
|
|
|
142
152
|
## Error Handling
|
|
143
153
|
|
|
154
|
+
Resource methods do not throw. Each returns a `Result`, a plain discriminated
|
|
155
|
+
union mirroring Zod's `safeParse`:
|
|
156
|
+
|
|
144
157
|
```typescript
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
RateLimitError,
|
|
150
|
-
NetworkError,
|
|
151
|
-
ServerError,
|
|
152
|
-
ValidationError,
|
|
153
|
-
} from '@rustrak/client';
|
|
158
|
+
type Result<T, E> =
|
|
159
|
+
| { success: true; data: T }
|
|
160
|
+
| { success: false; error: E };
|
|
161
|
+
```
|
|
154
162
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
+
Failures are one closed union keyed on `kind`, so the compiler forces you to
|
|
164
|
+
handle the case you care about and lets you ignore the rest deliberately:
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
import { isRetryable, type RustrakError } from '@rustrak/client';
|
|
168
|
+
|
|
169
|
+
const result = await client.projects.list();
|
|
170
|
+
|
|
171
|
+
if (!result.success) {
|
|
172
|
+
switch (result.error.kind) {
|
|
173
|
+
case 'unauthenticated':
|
|
174
|
+
redirect('/login');
|
|
175
|
+
break;
|
|
176
|
+
case 'rate_limited':
|
|
177
|
+
console.log(`Retry after ${result.error.retryAfter ?? '?'}s`);
|
|
178
|
+
break;
|
|
179
|
+
case 'not_found':
|
|
180
|
+
console.log('Gone');
|
|
181
|
+
break;
|
|
182
|
+
default:
|
|
183
|
+
if (isRetryable(result.error)) scheduleRetry();
|
|
184
|
+
else console.error(result.error.message);
|
|
185
|
+
}
|
|
186
|
+
return;
|
|
163
187
|
}
|
|
188
|
+
|
|
189
|
+
console.log(result.data.items);
|
|
164
190
|
```
|
|
165
191
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
| `
|
|
169
|
-
|
|
170
|
-
| `
|
|
171
|
-
| `
|
|
172
|
-
| `
|
|
173
|
-
| `
|
|
174
|
-
| `
|
|
175
|
-
| `
|
|
192
|
+
### The error union
|
|
193
|
+
|
|
194
|
+
| `kind` | HTTP | `status` | Retryable | Notes |
|
|
195
|
+
|---|---|---|---|---|
|
|
196
|
+
| `validation` | 400 | ✅ | ❌ | The server rejected the request |
|
|
197
|
+
| `unauthenticated` | 401 | ✅ | ❌ | No session, or it expired |
|
|
198
|
+
| `forbidden` | 403 | ✅ | ❌ | Authenticated but not allowed |
|
|
199
|
+
| `not_found` | 404 | ✅ | ❌ | |
|
|
200
|
+
| `conflict` | 409 | ✅ | ❌ | |
|
|
201
|
+
| `gone` | 410 | ✅ | ❌ | |
|
|
202
|
+
| `payload_too_large` | 413 | ✅ | ❌ | Envelope ingestion only |
|
|
203
|
+
| `rate_limited` | 429 | ✅ | ✅ | Optional `retryAfter` in seconds |
|
|
204
|
+
| `client_error` | other 4xx | ✅ | ❌ | Catch-all, so no status is unrepresentable |
|
|
205
|
+
| `server_error` | 5xx | ✅ | ✅ | **Message is always a fixed generic string** |
|
|
206
|
+
| `network` | n/a | no | ✅ | DNS, refused connection, TLS, timeout |
|
|
207
|
+
| `invalid_response` | n/a | no | ❌ | 2xx whose body failed its schema |
|
|
208
|
+
| `invalid_request` | n/a | no | ❌ | Your input failed a pre-flight check; nothing was sent |
|
|
209
|
+
|
|
210
|
+
`status` is `number`, never a literal, so a proxy-generated 502 or a future
|
|
211
|
+
status is a value you can log rather than a type error.
|
|
212
|
+
|
|
213
|
+
Three things the union deliberately does **not** carry, because each embeds
|
|
214
|
+
information that must not cross a serialization boundary:
|
|
215
|
+
|
|
216
|
+
- `server_error` never carries the server's message. `AppError::Internal` and
|
|
217
|
+
`AppError::Database` interpolate arbitrary internal text (a pool error, an OS
|
|
218
|
+
errno, a filesystem path); the client discards it at construction and
|
|
219
|
+
substitutes `SERVER_ERROR_MESSAGE`.
|
|
220
|
+
- `network` carries no `cause`. The underlying error embeds the resolved host
|
|
221
|
+
and port.
|
|
222
|
+
- `invalid_response` carries no Zod issues. They embed the offending response
|
|
223
|
+
data.
|
|
224
|
+
|
|
225
|
+
### Result helpers
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
import { Ok, Err, unwrap, unwrapOr, mapResult } from '@rustrak/client';
|
|
229
|
+
|
|
230
|
+
mapResult(await client.projects.get(1), (project) => project.name);
|
|
231
|
+
unwrap(await client.projects.get(1)); // throws on failure: opting back in
|
|
232
|
+
unwrapOr(await client.stats.summary(1, "24h"), null); // see the warning below
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
> **`unwrapOr` is not for rendering.** `unwrapOr(await client.projects.list(),
|
|
236
|
+
> [])` shows the same empty state for "this account has no projects" and "the
|
|
237
|
+
> server is unreachable": the exact confusion the `Result` API exists to
|
|
238
|
+
> prevent, one line after the failure was produced. Use it only where the
|
|
239
|
+
> fallback is correct regardless of *why* the call failed (a cached-count
|
|
240
|
+
> optimisation, a best-effort telemetry read, a script that already logged the
|
|
241
|
+
> error). Anywhere a human sees the outcome, branch on `result.success` and read
|
|
242
|
+
> `error.kind`.
|
|
243
|
+
|
|
244
|
+
Every value in a `Result` is a plain object with `Object.prototype`. That is
|
|
245
|
+
what lets a failure be returned from a Server Component or a Server Action and
|
|
246
|
+
read in a Client Component: React's Flight serializer rejects class instances,
|
|
247
|
+
which is why there are no error classes here.
|
|
176
248
|
|
|
177
249
|
## Next.js Integration
|
|
178
250
|
|
|
@@ -187,7 +259,16 @@ export default async function ProjectsPage() {
|
|
|
187
259
|
token: process.env.RUSTRAK_API_TOKEN!,
|
|
188
260
|
});
|
|
189
261
|
const projects = await client.projects.list();
|
|
190
|
-
|
|
262
|
+
|
|
263
|
+
if (!projects.success) {
|
|
264
|
+
// `kind` is what decides: only 'unauthenticated' means "send to login".
|
|
265
|
+
// Redirecting on 'network' or 'server_error' turns a flaky connection
|
|
266
|
+
// into a login loop.
|
|
267
|
+
if (projects.error.kind === 'unauthenticated') redirect('/auth/login');
|
|
268
|
+
return <LoadFailed error={projects.error} />;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return <ProjectsList projects={projects.data.items} />;
|
|
191
272
|
}
|
|
192
273
|
```
|
|
193
274
|
|
|
@@ -202,6 +283,8 @@ export async function resolveIssue(projectId: number, issueId: string) {
|
|
|
202
283
|
baseUrl: process.env.RUSTRAK_API_URL!,
|
|
203
284
|
token: process.env.RUSTRAK_API_TOKEN!,
|
|
204
285
|
});
|
|
286
|
+
// The Result is returned as-is: it is serializable, so the client component
|
|
287
|
+
// receives the failure instead of an opaque "An error occurred" digest.
|
|
205
288
|
return client.issues.updateState(projectId, issueId, { is_resolved: true });
|
|
206
289
|
}
|
|
207
290
|
```
|
|
@@ -220,6 +303,9 @@ import type {
|
|
|
220
303
|
PaginatedResponse,
|
|
221
304
|
CreateProject,
|
|
222
305
|
UpdateIssueState,
|
|
306
|
+
Result,
|
|
307
|
+
RustrakError,
|
|
308
|
+
RustrakErrorKind,
|
|
223
309
|
} from '@rustrak/client';
|
|
224
310
|
```
|
|
225
311
|
|