@rallycry/conveyor-incidents 0.3.1 → 0.3.3
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 +62 -0
- package/dist/index.d.ts +51 -1
- package/dist/index.js +189 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -91,6 +91,29 @@ await incidents.report({
|
|
|
91
91
|
});
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
+
### Reporter metadata (`reporter`)
|
|
95
|
+
|
|
96
|
+
For any **user-initiated** report, send who filed it and what page they were on:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
await incidents.report({
|
|
100
|
+
title: "Riot login fails",
|
|
101
|
+
description: userMessage,
|
|
102
|
+
reporter: {
|
|
103
|
+
name: currentUser.displayName,
|
|
104
|
+
email: currentUser.email,
|
|
105
|
+
url: window.location.href,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Conveyor renders these as `Reported by:` / `URL:` lines in the card description's
|
|
111
|
+
`---` metadata footer, and surfaces them on the card's Slack message. Unlike
|
|
112
|
+
`createdBy`, no project membership is required — this is descriptive metadata,
|
|
113
|
+
so an end-user report keeps an identity instead of reading as an anonymous
|
|
114
|
+
automated alert. Lines your own footer already writes (e.g. your own `URL:`) are
|
|
115
|
+
not duplicated, and `url` must be an absolute `http(s)` URL.
|
|
116
|
+
|
|
94
117
|
### About `source` and `fingerprintKey`
|
|
95
118
|
|
|
96
119
|
Conveyor deduplicates reports by a fingerprint derived from `(projectId, source, fingerprintKey)` — or by `(projectId, source, title)` if `fingerprintKey` is omitted. **Use stable values** for `source` (e.g. `"prod-web"`, not `"node-pid-1234"`) so that repeat reports collapse into a single incident instead of creating noise.
|
|
@@ -100,6 +123,29 @@ Conveyor deduplicates reports by a fingerprint derived from `(projectId, source,
|
|
|
100
123
|
- **5 MB per file** (`MAX_ATTACHMENT_BYTES`, exported). Oversized files are rejected client-side with a descriptive `attachmentResults` error — no HTTP call is made.
|
|
101
124
|
- **10 attachments per card** (server-enforced).
|
|
102
125
|
|
|
126
|
+
### Browser network evidence
|
|
127
|
+
|
|
128
|
+
For browser-based incident reporters, opt into the bounded fetch recorder early in your app shell and attach its log when the user reports a bug:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import {
|
|
132
|
+
buildNetworkLogAttachment,
|
|
133
|
+
installNetworkCapture,
|
|
134
|
+
} from "@rallycry/conveyor-incidents";
|
|
135
|
+
|
|
136
|
+
installNetworkCapture({ maxEntries: 120, maxBodyChars: 6000 });
|
|
137
|
+
|
|
138
|
+
await incidents.report({
|
|
139
|
+
title: "Competition page showed 403",
|
|
140
|
+
attachments: [
|
|
141
|
+
buildNetworkLogAttachment(),
|
|
142
|
+
// ...context.json, screenshot.jpg, console.log, etc.
|
|
143
|
+
].filter((it): it is NonNullable<typeof it> => it !== undefined),
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`network.log` is newline-delimited JSON. Each entry includes method, URL with sensitive query parameters redacted, status, duration, `traceparent` headers when available, tRPC procedure names for `/trpc/a,b,c` batches, and capped response snippets for failed responses and tRPC batches. This is intentionally diagnostic, not a full traffic archive.
|
|
148
|
+
|
|
103
149
|
### Error handling
|
|
104
150
|
|
|
105
151
|
- The main `report()` call throws `ConveyorIncidentsError` on non-2xx responses; it exposes `.status` and `.body`.
|
|
@@ -138,6 +184,11 @@ client.report({
|
|
|
138
184
|
assigned?: string[]; // project-member emails; first match becomes assignee
|
|
139
185
|
review?: string[]; // project-member emails; every match becomes a reviewer
|
|
140
186
|
createdBy?: string; // project-member email; no match → "Conveyor Incidents" system user
|
|
187
|
+
reporter?: { // who filed it + where; rendered into the card's metadata footer
|
|
188
|
+
name?: string;
|
|
189
|
+
email?: string;
|
|
190
|
+
url?: string; // absolute http(s) URL of the page at report time
|
|
191
|
+
};
|
|
141
192
|
attachments?: Array<{
|
|
142
193
|
fileName: string;
|
|
143
194
|
mimeType: string;
|
|
@@ -153,6 +204,17 @@ client.report({
|
|
|
153
204
|
error?: string;
|
|
154
205
|
}>;
|
|
155
206
|
}>;
|
|
207
|
+
|
|
208
|
+
installNetworkCapture({
|
|
209
|
+
maxEntries?: number; // default: 80
|
|
210
|
+
maxBodyChars?: number; // default: 4000
|
|
211
|
+
captureTrpcResponses?: boolean; // default: true
|
|
212
|
+
}): void;
|
|
213
|
+
|
|
214
|
+
getNetworkLogEntries(): NetworkLogEntry[];
|
|
215
|
+
clearNetworkLogEntries(): void;
|
|
216
|
+
uninstallNetworkCapture(): void;
|
|
217
|
+
buildNetworkLogAttachment(fileName?: string): IncidentAttachment | undefined;
|
|
156
218
|
```
|
|
157
219
|
|
|
158
220
|
## License
|
package/dist/index.d.ts
CHANGED
|
@@ -21,6 +21,45 @@ interface IncidentAttachment {
|
|
|
21
21
|
mimeType: string;
|
|
22
22
|
content: Uint8Array | string;
|
|
23
23
|
}
|
|
24
|
+
interface NetworkCaptureOptions {
|
|
25
|
+
/** Maximum entries kept in memory. Defaults to 80. */
|
|
26
|
+
maxEntries?: number;
|
|
27
|
+
/** Maximum response body characters kept per captured entry. Defaults to 4000. */
|
|
28
|
+
maxBodyChars?: number;
|
|
29
|
+
/** Capture response snippets for successful tRPC batch calls as well as failures. Defaults to true. */
|
|
30
|
+
captureTrpcResponses?: boolean;
|
|
31
|
+
}
|
|
32
|
+
interface NetworkLogEntry {
|
|
33
|
+
timestamp: string;
|
|
34
|
+
kind: "fetch";
|
|
35
|
+
method: string;
|
|
36
|
+
url: string;
|
|
37
|
+
status: number | null;
|
|
38
|
+
ok: boolean | null;
|
|
39
|
+
durationMs: number;
|
|
40
|
+
responseBytes: number | null;
|
|
41
|
+
contentType?: string;
|
|
42
|
+
requestTraceparent?: string;
|
|
43
|
+
responseTraceparent?: string;
|
|
44
|
+
trpcProcedures?: string[];
|
|
45
|
+
responseSnippet?: string;
|
|
46
|
+
error?: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Who filed the report and where they were when they filed it. Unlike
|
|
50
|
+
* `createdBy`, this needs no Conveyor project membership — it's descriptive
|
|
51
|
+
* metadata, rendered into the card's description footer and onto the Slack
|
|
52
|
+
* card. Send it for every user-initiated report: without it a card from an
|
|
53
|
+
* end user is indistinguishable from an automated alert.
|
|
54
|
+
*/
|
|
55
|
+
interface ReportReporter {
|
|
56
|
+
/** Display name / username of the person filing the report. */
|
|
57
|
+
name?: string;
|
|
58
|
+
/** Contact email. Also used for attribution when it matches a member. */
|
|
59
|
+
email?: string;
|
|
60
|
+
/** Absolute http(s) URL of the page the user was on at report time. */
|
|
61
|
+
url?: string;
|
|
62
|
+
}
|
|
24
63
|
interface ReportIncidentInput {
|
|
25
64
|
title: string;
|
|
26
65
|
description?: string;
|
|
@@ -49,6 +88,12 @@ interface ReportIncidentInput {
|
|
|
49
88
|
* is attributed to the "Conveyor Incidents" system user.
|
|
50
89
|
*/
|
|
51
90
|
createdBy?: string;
|
|
91
|
+
/**
|
|
92
|
+
* Reporter identity + the page they were on. Rendered into the card's
|
|
93
|
+
* description metadata footer (and the Slack card) by the server; lines the
|
|
94
|
+
* description already carries are not duplicated.
|
|
95
|
+
*/
|
|
96
|
+
reporter?: ReportReporter;
|
|
52
97
|
}
|
|
53
98
|
interface AttachmentResult {
|
|
54
99
|
fileName: string;
|
|
@@ -62,6 +107,11 @@ interface ReportIncidentResult {
|
|
|
62
107
|
cardId: string;
|
|
63
108
|
attachmentResults: AttachmentResult[];
|
|
64
109
|
}
|
|
110
|
+
declare function installNetworkCapture(options?: NetworkCaptureOptions): void;
|
|
111
|
+
declare function uninstallNetworkCapture(): void;
|
|
112
|
+
declare function clearNetworkLogEntries(): void;
|
|
113
|
+
declare function getNetworkLogEntries(): NetworkLogEntry[];
|
|
114
|
+
declare function buildNetworkLogAttachment(fileName?: string): IncidentAttachment | undefined;
|
|
65
115
|
declare class ConveyorIncidents {
|
|
66
116
|
private readonly apiKey;
|
|
67
117
|
private readonly projectId;
|
|
@@ -72,4 +122,4 @@ declare class ConveyorIncidents {
|
|
|
72
122
|
private uploadAttachment;
|
|
73
123
|
}
|
|
74
124
|
|
|
75
|
-
export { type AttachmentResult, type ConveyorCardType, ConveyorIncidents, ConveyorIncidentsError, type ConveyorIncidentsOptions, type IncidentAttachment, type IncidentSeverity, MAX_ATTACHMENT_BYTES, type ReportIncidentInput, type ReportIncidentResult };
|
|
125
|
+
export { type AttachmentResult, type ConveyorCardType, ConveyorIncidents, ConveyorIncidentsError, type ConveyorIncidentsOptions, type IncidentAttachment, type IncidentSeverity, MAX_ATTACHMENT_BYTES, type NetworkCaptureOptions, type NetworkLogEntry, type ReportIncidentInput, type ReportIncidentResult, type ReportReporter, buildNetworkLogAttachment, clearNetworkLogEntries, getNetworkLogEntries, installNetworkCapture, uninstallNetworkCapture };
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,85 @@ var DEFAULT_BASE_URL = "https://api.conveyor.rallycryapp.com";
|
|
|
15
15
|
var DEFAULT_SOURCE = "client";
|
|
16
16
|
var DEFAULT_SEVERITY = "medium";
|
|
17
17
|
var MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;
|
|
18
|
+
var networkCaptureState;
|
|
19
|
+
var DEFAULT_NETWORK_CAPTURE_OPTIONS = {
|
|
20
|
+
maxEntries: 80,
|
|
21
|
+
maxBodyChars: 4e3,
|
|
22
|
+
captureTrpcResponses: true
|
|
23
|
+
};
|
|
24
|
+
function installNetworkCapture(options = {}) {
|
|
25
|
+
if (typeof globalThis.fetch !== "function") return;
|
|
26
|
+
const fetchImpl = globalThis.fetch.bind(globalThis);
|
|
27
|
+
const resolvedOptions = {
|
|
28
|
+
...DEFAULT_NETWORK_CAPTURE_OPTIONS,
|
|
29
|
+
...options
|
|
30
|
+
};
|
|
31
|
+
if (networkCaptureState) {
|
|
32
|
+
networkCaptureState.options = resolvedOptions;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const entries = [];
|
|
36
|
+
networkCaptureState = {
|
|
37
|
+
installed: true,
|
|
38
|
+
originalFetch: fetchImpl,
|
|
39
|
+
entries,
|
|
40
|
+
options: resolvedOptions
|
|
41
|
+
};
|
|
42
|
+
globalThis.fetch = async (input, init) => {
|
|
43
|
+
const state = networkCaptureState;
|
|
44
|
+
const originalFetch = state?.originalFetch ?? fetchImpl;
|
|
45
|
+
const startedAt = Date.now();
|
|
46
|
+
const timestamp = new Date(startedAt).toISOString();
|
|
47
|
+
const request = normalizeRequest(input, init);
|
|
48
|
+
try {
|
|
49
|
+
const response = await originalFetch(input, init);
|
|
50
|
+
const entry = await buildNetworkEntry({
|
|
51
|
+
timestamp,
|
|
52
|
+
request,
|
|
53
|
+
response,
|
|
54
|
+
durationMs: Date.now() - startedAt,
|
|
55
|
+
options: state?.options ?? resolvedOptions
|
|
56
|
+
});
|
|
57
|
+
pushNetworkEntry(entry);
|
|
58
|
+
return response;
|
|
59
|
+
} catch (err) {
|
|
60
|
+
pushNetworkEntry({
|
|
61
|
+
timestamp,
|
|
62
|
+
kind: "fetch",
|
|
63
|
+
method: request.method,
|
|
64
|
+
url: redactUrl(request.url),
|
|
65
|
+
status: null,
|
|
66
|
+
ok: null,
|
|
67
|
+
durationMs: Date.now() - startedAt,
|
|
68
|
+
responseBytes: null,
|
|
69
|
+
requestTraceparent: request.traceparent,
|
|
70
|
+
trpcProcedures: extractTrpcProcedures(request.url),
|
|
71
|
+
error: err instanceof Error ? err.message : String(err)
|
|
72
|
+
});
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function uninstallNetworkCapture() {
|
|
78
|
+
if (!networkCaptureState) return;
|
|
79
|
+
globalThis.fetch = networkCaptureState.originalFetch;
|
|
80
|
+
networkCaptureState = void 0;
|
|
81
|
+
}
|
|
82
|
+
function clearNetworkLogEntries() {
|
|
83
|
+
networkCaptureState?.entries.splice(0);
|
|
84
|
+
}
|
|
85
|
+
function getNetworkLogEntries() {
|
|
86
|
+
return [...networkCaptureState?.entries ?? []];
|
|
87
|
+
}
|
|
88
|
+
function buildNetworkLogAttachment(fileName = "network.log") {
|
|
89
|
+
const entries = getNetworkLogEntries();
|
|
90
|
+
if (entries.length === 0) return void 0;
|
|
91
|
+
return {
|
|
92
|
+
fileName,
|
|
93
|
+
mimeType: "application/x-ndjson",
|
|
94
|
+
content: entries.map((entry) => JSON.stringify(entry)).join("\n")
|
|
95
|
+
};
|
|
96
|
+
}
|
|
18
97
|
var ConveyorIncidents = class {
|
|
19
98
|
apiKey;
|
|
20
99
|
projectId;
|
|
@@ -48,7 +127,8 @@ var ConveyorIncidents = class {
|
|
|
48
127
|
type: input.type,
|
|
49
128
|
assigned: input.assigned,
|
|
50
129
|
review: input.review,
|
|
51
|
-
createdBy: input.createdBy
|
|
130
|
+
createdBy: input.createdBy,
|
|
131
|
+
reporter: normalizeReporter(input.reporter)
|
|
52
132
|
};
|
|
53
133
|
const res = await this.fetchImpl(`${this.baseUrl}/api/incidents/report`, {
|
|
54
134
|
method: "POST",
|
|
@@ -127,9 +207,111 @@ var ConveyorIncidents = class {
|
|
|
127
207
|
}
|
|
128
208
|
}
|
|
129
209
|
};
|
|
210
|
+
function pushNetworkEntry(entry) {
|
|
211
|
+
const state = networkCaptureState;
|
|
212
|
+
if (!state) return;
|
|
213
|
+
state.entries.push(entry);
|
|
214
|
+
const overflow = state.entries.length - state.options.maxEntries;
|
|
215
|
+
if (overflow > 0) state.entries.splice(0, overflow);
|
|
216
|
+
}
|
|
217
|
+
function normalizeRequest(input, init) {
|
|
218
|
+
const request = typeof Request !== "undefined" && input instanceof Request ? input : void 0;
|
|
219
|
+
const method = init?.method ?? request?.method ?? "GET";
|
|
220
|
+
const url = request?.url ?? String(input);
|
|
221
|
+
const traceparent = readHeader(init?.headers, "traceparent") ?? readHeader(request?.headers, "traceparent");
|
|
222
|
+
return { method: method.toUpperCase(), url, traceparent };
|
|
223
|
+
}
|
|
224
|
+
async function buildNetworkEntry(params) {
|
|
225
|
+
const { timestamp, request, response, durationMs, options } = params;
|
|
226
|
+
const contentType = response.headers.get("content-type") ?? void 0;
|
|
227
|
+
const trpcProcedures = extractTrpcProcedures(request.url);
|
|
228
|
+
const shouldCaptureBody = !response.ok || options.captureTrpcResponses && trpcProcedures !== void 0;
|
|
229
|
+
const bodyText = shouldCaptureBody ? await readResponseSnippet(response, options.maxBodyChars) : void 0;
|
|
230
|
+
const responseBytes = readContentLength(response.headers) ?? estimateUtf8Bytes(bodyText);
|
|
231
|
+
return {
|
|
232
|
+
timestamp,
|
|
233
|
+
kind: "fetch",
|
|
234
|
+
method: request.method,
|
|
235
|
+
url: redactUrl(request.url),
|
|
236
|
+
status: response.status,
|
|
237
|
+
ok: response.ok,
|
|
238
|
+
durationMs,
|
|
239
|
+
responseBytes,
|
|
240
|
+
contentType,
|
|
241
|
+
requestTraceparent: request.traceparent,
|
|
242
|
+
responseTraceparent: response.headers.get("traceparent") ?? void 0,
|
|
243
|
+
trpcProcedures,
|
|
244
|
+
responseSnippet: bodyText
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function readHeader(headers, name) {
|
|
248
|
+
if (!headers) return void 0;
|
|
249
|
+
if (headers instanceof Headers) return headers.get(name) ?? void 0;
|
|
250
|
+
const lowerName = name.toLowerCase();
|
|
251
|
+
if (Array.isArray(headers)) {
|
|
252
|
+
const hit = headers.find(([key]) => key.toLowerCase() === lowerName);
|
|
253
|
+
return hit?.[1];
|
|
254
|
+
}
|
|
255
|
+
const value = Object.entries(headers).find(([key]) => key.toLowerCase() === lowerName)?.[1];
|
|
256
|
+
return value;
|
|
257
|
+
}
|
|
258
|
+
function readContentLength(headers) {
|
|
259
|
+
const raw = headers.get("content-length");
|
|
260
|
+
if (!raw) return null;
|
|
261
|
+
const parsed = Number.parseInt(raw, 10);
|
|
262
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
263
|
+
}
|
|
264
|
+
function estimateUtf8Bytes(value) {
|
|
265
|
+
if (value === void 0) return null;
|
|
266
|
+
return new TextEncoder().encode(value).byteLength;
|
|
267
|
+
}
|
|
268
|
+
async function readResponseSnippet(response, maxChars) {
|
|
269
|
+
try {
|
|
270
|
+
const text = await response.clone().text();
|
|
271
|
+
if (text.length <= maxChars) return text;
|
|
272
|
+
return `${text.slice(0, maxChars)}...[truncated ${String(text.length - maxChars)} chars]`;
|
|
273
|
+
} catch {
|
|
274
|
+
return void 0;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function extractTrpcProcedures(rawUrl) {
|
|
278
|
+
let url;
|
|
279
|
+
try {
|
|
280
|
+
url = new URL(rawUrl, "https://placeholder.invalid");
|
|
281
|
+
} catch {
|
|
282
|
+
return void 0;
|
|
283
|
+
}
|
|
284
|
+
const marker = "/trpc/";
|
|
285
|
+
const index = url.pathname.indexOf(marker);
|
|
286
|
+
if (index === -1) return void 0;
|
|
287
|
+
const path = url.pathname.slice(index + marker.length);
|
|
288
|
+
const procedures = path.split(",").map((part) => decodeURIComponent(part).trim()).filter((part) => part.length > 0);
|
|
289
|
+
return procedures.length > 0 ? procedures : void 0;
|
|
290
|
+
}
|
|
291
|
+
function redactUrl(rawUrl) {
|
|
292
|
+
let url;
|
|
293
|
+
try {
|
|
294
|
+
url = new URL(rawUrl, "https://placeholder.invalid");
|
|
295
|
+
} catch {
|
|
296
|
+
return rawUrl;
|
|
297
|
+
}
|
|
298
|
+
for (const key of [...url.searchParams.keys()]) {
|
|
299
|
+
const lower = key.toLowerCase();
|
|
300
|
+
if (lower.includes("token") || lower.includes("key") || lower.includes("secret") || lower.includes("signature") || lower.includes("sig")) {
|
|
301
|
+
url.searchParams.set(key, "[redacted]");
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return url.origin === "https://placeholder.invalid" ? `${url.pathname}${url.search}` : url.toString();
|
|
305
|
+
}
|
|
130
306
|
function stripTrailingSlash(url) {
|
|
131
307
|
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
132
308
|
}
|
|
309
|
+
function normalizeReporter(reporter) {
|
|
310
|
+
if (!reporter) return void 0;
|
|
311
|
+
const entries = ["name", "email", "url"].map((key) => [key, reporter[key]?.trim()]).filter((entry) => !!entry[1]);
|
|
312
|
+
if (entries.length === 0) return void 0;
|
|
313
|
+
return Object.fromEntries(entries);
|
|
314
|
+
}
|
|
133
315
|
function extractId(payload, key) {
|
|
134
316
|
if (payload && typeof payload === "object" && key in payload) {
|
|
135
317
|
const value = payload[key];
|
|
@@ -169,6 +351,11 @@ function bytesToBase64(bytes) {
|
|
|
169
351
|
export {
|
|
170
352
|
ConveyorIncidents,
|
|
171
353
|
ConveyorIncidentsError,
|
|
172
|
-
MAX_ATTACHMENT_BYTES
|
|
354
|
+
MAX_ATTACHMENT_BYTES,
|
|
355
|
+
buildNetworkLogAttachment,
|
|
356
|
+
clearNetworkLogEntries,
|
|
357
|
+
getNetworkLogEntries,
|
|
358
|
+
installNetworkCapture,
|
|
359
|
+
uninstallNetworkCapture
|
|
173
360
|
};
|
|
174
361
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/errors.ts","../src/index.ts"],"sourcesContent":["export class ConveyorIncidentsError extends Error {\n readonly status: number;\n readonly body: unknown;\n\n constructor(message: string, status: number, body: unknown) {\n super(message);\n this.name = \"ConveyorIncidentsError\";\n this.status = status;\n this.body = body;\n }\n}\n","import { ConveyorIncidentsError } from \"./errors.js\";\n\nexport { ConveyorIncidentsError } from \"./errors.js\";\n\nconst DEFAULT_BASE_URL = \"https://api.conveyor.rallycryapp.com\";\nconst DEFAULT_SOURCE = \"client\";\nconst DEFAULT_SEVERITY: IncidentSeverity = \"medium\";\n\n/** Server-side per-file ceiling — keep in sync with MAX_ATTACHMENT_BYTES in\n * conveyor's incident-routes.ts. Checked client-side so an oversized file\n * fails with a descriptive `attachmentResults` error instead of an HTTP 413. */\nexport const MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;\n\nexport type IncidentSeverity = \"critical\" | \"high\" | \"medium\" | \"low\";\n\nexport type ConveyorCardType = \"incident\" | \"suggestion\" | \"task\";\n\nexport interface ConveyorIncidentsOptions {\n apiKey: string;\n projectId: string;\n baseUrl?: string;\n fetch?: typeof fetch;\n}\n\nexport interface IncidentAttachment {\n fileName: string;\n mimeType: string;\n content: Uint8Array | string;\n}\n\nexport interface ReportIncidentInput {\n title: string;\n description?: string;\n severity?: IncidentSeverity;\n source?: string;\n fingerprintKey?: string;\n plan?: string;\n attachments?: IncidentAttachment[];\n /** Card type to create. The server defaults to \"incident\" when omitted. */\n type?: ConveyorCardType;\n /**\n * Project-member emails. The FIRST email that resolves to a member of the\n * target project becomes the card's assignee; the rest are ignored.\n * Non-member emails are silently skipped.\n */\n assigned?: string[];\n /**\n * Project-member emails. EVERY email that resolves to a member of the\n * target project is added as a reviewer. Non-member emails are silently\n * skipped.\n */\n review?: string[];\n /**\n * Project-member email used as the card's creator. When omitted — or when\n * the email doesn't resolve to a member of the target project — the card\n * is attributed to the \"Conveyor Incidents\" system user.\n */\n createdBy?: string;\n}\n\nexport interface AttachmentResult {\n fileName: string;\n ok: boolean;\n fileId?: string;\n error?: string;\n}\n\nexport interface ReportIncidentResult {\n incidentId: string;\n /** Alias of `incidentId` — prefer this when reporting type \"suggestion\" or \"task\". */\n cardId: string;\n attachmentResults: AttachmentResult[];\n}\n\nexport class ConveyorIncidents {\n private readonly apiKey: string;\n private readonly projectId: string;\n private readonly baseUrl: string;\n private readonly fetchImpl: typeof fetch;\n\n constructor(opts: ConveyorIncidentsOptions) {\n if (!opts.apiKey) {\n throw new Error(\"ConveyorIncidents: apiKey is required\");\n }\n if (!opts.projectId) {\n throw new Error(\"ConveyorIncidents: projectId is required\");\n }\n const resolvedFetch = opts.fetch ?? globalThis.fetch;\n if (typeof resolvedFetch !== \"function\") {\n throw new Error(\"ConveyorIncidents: no global fetch available — pass opts.fetch on Node <18\");\n }\n this.apiKey = opts.apiKey;\n this.projectId = opts.projectId;\n this.baseUrl = stripTrailingSlash(opts.baseUrl ?? DEFAULT_BASE_URL);\n this.fetchImpl = resolvedFetch;\n }\n\n async report(input: ReportIncidentInput): Promise<ReportIncidentResult> {\n const body = {\n projectId: this.projectId,\n source: input.source ?? DEFAULT_SOURCE,\n severity: input.severity ?? DEFAULT_SEVERITY,\n title: input.title,\n description: input.description,\n plan: input.plan,\n fingerprintKey: input.fingerprintKey,\n type: input.type,\n assigned: input.assigned,\n review: input.review,\n createdBy: input.createdBy,\n };\n\n const res = await this.fetchImpl(`${this.baseUrl}/api/incidents/report`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify(body),\n });\n\n const payload = await readJson(res);\n\n if (!res.ok) {\n throw new ConveyorIncidentsError(\n `Failed to report incident: ${res.status} ${res.statusText}`,\n res.status,\n payload,\n );\n }\n\n const incidentId = extractId(payload, \"incidentId\") ?? extractId(payload, \"cardId\");\n\n if (!incidentId) {\n throw new ConveyorIncidentsError(\n \"Invalid response from /api/incidents/report: missing incidentId\",\n res.status,\n payload,\n );\n }\n\n const attachmentResults: AttachmentResult[] = [];\n if (input.attachments && input.attachments.length > 0) {\n for (const attachment of input.attachments) {\n attachmentResults.push(await this.uploadAttachment(incidentId, attachment));\n }\n }\n\n return { incidentId, cardId: incidentId, attachmentResults };\n }\n\n private async uploadAttachment(\n incidentId: string,\n attachment: IncidentAttachment,\n ): Promise<AttachmentResult> {\n try {\n const bytes =\n typeof attachment.content === \"string\"\n ? new TextEncoder().encode(attachment.content)\n : attachment.content;\n if (bytes.byteLength > MAX_ATTACHMENT_BYTES) {\n return {\n fileName: attachment.fileName,\n ok: false,\n error: `Attachment is ${bytes.byteLength} bytes — exceeds the ${MAX_ATTACHMENT_BYTES}-byte (5 MB) server limit; upload skipped`,\n };\n }\n const contentBase64 = bytesToBase64(bytes);\n const res = await this.fetchImpl(\n `${this.baseUrl}/api/incidents/${encodeURIComponent(incidentId)}/files`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify({\n fileName: attachment.fileName,\n mimeType: attachment.mimeType,\n contentBase64,\n }),\n },\n );\n\n const payload = await readJson(res);\n\n if (!res.ok) {\n return {\n fileName: attachment.fileName,\n ok: false,\n error: formatErrorBody(res.status, res.statusText, payload),\n };\n }\n\n const fileId =\n payload && typeof payload === \"object\" && \"fileId\" in payload\n ? String((payload as { fileId: unknown }).fileId)\n : undefined;\n\n return { fileName: attachment.fileName, ok: true, fileId };\n } catch (err) {\n return {\n fileName: attachment.fileName,\n ok: false,\n error: err instanceof Error ? err.message : String(err),\n };\n }\n }\n}\n\nfunction stripTrailingSlash(url: string): string {\n return url.endsWith(\"/\") ? url.slice(0, -1) : url;\n}\n\nfunction extractId(payload: unknown, key: \"incidentId\" | \"cardId\"): string | undefined {\n if (payload && typeof payload === \"object\" && key in payload) {\n const value = (payload as Record<string, unknown>)[key];\n if (value !== undefined && value !== null) return String(value);\n }\n return undefined;\n}\n\nasync function readJson(res: Response): Promise<unknown> {\n const text = await res.text();\n if (!text) return undefined;\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n\nfunction formatErrorBody(status: number, statusText: string, body: unknown): string {\n const prefix = `${status} ${statusText}`.trim();\n if (body && typeof body === \"object\" && \"error\" in body) {\n return `${prefix}: ${String((body as { error: unknown }).error)}`;\n }\n if (typeof body === \"string\" && body.length > 0) {\n return `${prefix}: ${body}`;\n }\n return prefix;\n}\n\nfunction bytesToBase64(bytes: Uint8Array): string {\n if (typeof Buffer !== \"undefined\") {\n return Buffer.from(bytes).toString(\"base64\");\n }\n let binary = \"\";\n for (const byte of bytes) {\n binary += String.fromCharCode(byte);\n }\n return btoa(binary);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/index.ts"],"sourcesContent":["export class ConveyorIncidentsError extends Error {\n readonly status: number;\n readonly body: unknown;\n\n constructor(message: string, status: number, body: unknown) {\n super(message);\n this.name = \"ConveyorIncidentsError\";\n this.status = status;\n this.body = body;\n }\n}\n","import { ConveyorIncidentsError } from \"./errors.js\";\n\nexport { ConveyorIncidentsError } from \"./errors.js\";\n\nconst DEFAULT_BASE_URL = \"https://api.conveyor.rallycryapp.com\";\nconst DEFAULT_SOURCE = \"client\";\nconst DEFAULT_SEVERITY: IncidentSeverity = \"medium\";\n\n/** Server-side per-file ceiling — keep in sync with MAX_ATTACHMENT_BYTES in\n * conveyor's incident-routes.ts. Checked client-side so an oversized file\n * fails with a descriptive `attachmentResults` error instead of an HTTP 413. */\nexport const MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;\n\nexport type IncidentSeverity = \"critical\" | \"high\" | \"medium\" | \"low\";\n\nexport type ConveyorCardType = \"incident\" | \"suggestion\" | \"task\";\n\nexport interface ConveyorIncidentsOptions {\n apiKey: string;\n projectId: string;\n baseUrl?: string;\n fetch?: typeof fetch;\n}\n\nexport interface IncidentAttachment {\n fileName: string;\n mimeType: string;\n content: Uint8Array | string;\n}\n\nexport interface NetworkCaptureOptions {\n /** Maximum entries kept in memory. Defaults to 80. */\n maxEntries?: number;\n /** Maximum response body characters kept per captured entry. Defaults to 4000. */\n maxBodyChars?: number;\n /** Capture response snippets for successful tRPC batch calls as well as failures. Defaults to true. */\n captureTrpcResponses?: boolean;\n}\n\nexport interface NetworkLogEntry {\n timestamp: string;\n kind: \"fetch\";\n method: string;\n url: string;\n status: number | null;\n ok: boolean | null;\n durationMs: number;\n responseBytes: number | null;\n contentType?: string;\n requestTraceparent?: string;\n responseTraceparent?: string;\n trpcProcedures?: string[];\n responseSnippet?: string;\n error?: string;\n}\n\n/**\n * Who filed the report and where they were when they filed it. Unlike\n * `createdBy`, this needs no Conveyor project membership — it's descriptive\n * metadata, rendered into the card's description footer and onto the Slack\n * card. Send it for every user-initiated report: without it a card from an\n * end user is indistinguishable from an automated alert.\n */\nexport interface ReportReporter {\n /** Display name / username of the person filing the report. */\n name?: string;\n /** Contact email. Also used for attribution when it matches a member. */\n email?: string;\n /** Absolute http(s) URL of the page the user was on at report time. */\n url?: string;\n}\n\nexport interface ReportIncidentInput {\n title: string;\n description?: string;\n severity?: IncidentSeverity;\n source?: string;\n fingerprintKey?: string;\n plan?: string;\n attachments?: IncidentAttachment[];\n /** Card type to create. The server defaults to \"incident\" when omitted. */\n type?: ConveyorCardType;\n /**\n * Project-member emails. The FIRST email that resolves to a member of the\n * target project becomes the card's assignee; the rest are ignored.\n * Non-member emails are silently skipped.\n */\n assigned?: string[];\n /**\n * Project-member emails. EVERY email that resolves to a member of the\n * target project is added as a reviewer. Non-member emails are silently\n * skipped.\n */\n review?: string[];\n /**\n * Project-member email used as the card's creator. When omitted — or when\n * the email doesn't resolve to a member of the target project — the card\n * is attributed to the \"Conveyor Incidents\" system user.\n */\n createdBy?: string;\n /**\n * Reporter identity + the page they were on. Rendered into the card's\n * description metadata footer (and the Slack card) by the server; lines the\n * description already carries are not duplicated.\n */\n reporter?: ReportReporter;\n}\n\nexport interface AttachmentResult {\n fileName: string;\n ok: boolean;\n fileId?: string;\n error?: string;\n}\n\nexport interface ReportIncidentResult {\n incidentId: string;\n /** Alias of `incidentId` — prefer this when reporting type \"suggestion\" or \"task\". */\n cardId: string;\n attachmentResults: AttachmentResult[];\n}\n\nlet networkCaptureState:\n | {\n installed: true;\n originalFetch: typeof fetch;\n entries: NetworkLogEntry[];\n options: Required<NetworkCaptureOptions>;\n }\n | undefined;\n\nconst DEFAULT_NETWORK_CAPTURE_OPTIONS: Required<NetworkCaptureOptions> = {\n maxEntries: 80,\n maxBodyChars: 4000,\n captureTrpcResponses: true,\n};\n\nexport function installNetworkCapture(options: NetworkCaptureOptions = {}): void {\n if (typeof globalThis.fetch !== \"function\") return;\n // Bind to globalThis: some browsers (notably Safari/WebKit) throw\n // \"Illegal invocation\" when the native fetch is called without its\n // original receiver, which a bare captured reference loses.\n const fetchImpl = globalThis.fetch.bind(globalThis);\n\n const resolvedOptions = {\n ...DEFAULT_NETWORK_CAPTURE_OPTIONS,\n ...options,\n };\n\n if (networkCaptureState) {\n networkCaptureState.options = resolvedOptions;\n return;\n }\n\n const entries: NetworkLogEntry[] = [];\n networkCaptureState = {\n installed: true,\n originalFetch: fetchImpl,\n entries,\n options: resolvedOptions,\n };\n\n globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {\n const state = networkCaptureState;\n const originalFetch = state?.originalFetch ?? fetchImpl;\n const startedAt = Date.now();\n const timestamp = new Date(startedAt).toISOString();\n const request = normalizeRequest(input, init);\n\n try {\n const response = await originalFetch(input, init);\n const entry = await buildNetworkEntry({\n timestamp,\n request,\n response,\n durationMs: Date.now() - startedAt,\n options: state?.options ?? resolvedOptions,\n });\n pushNetworkEntry(entry);\n return response;\n } catch (err) {\n pushNetworkEntry({\n timestamp,\n kind: \"fetch\",\n method: request.method,\n url: redactUrl(request.url),\n status: null,\n ok: null,\n durationMs: Date.now() - startedAt,\n responseBytes: null,\n requestTraceparent: request.traceparent,\n trpcProcedures: extractTrpcProcedures(request.url),\n error: err instanceof Error ? err.message : String(err),\n });\n throw err;\n }\n };\n}\n\nexport function uninstallNetworkCapture(): void {\n if (!networkCaptureState) return;\n globalThis.fetch = networkCaptureState.originalFetch;\n networkCaptureState = undefined;\n}\n\nexport function clearNetworkLogEntries(): void {\n networkCaptureState?.entries.splice(0);\n}\n\nexport function getNetworkLogEntries(): NetworkLogEntry[] {\n return [...(networkCaptureState?.entries ?? [])];\n}\n\nexport function buildNetworkLogAttachment(\n fileName = \"network.log\",\n): IncidentAttachment | undefined {\n const entries = getNetworkLogEntries();\n if (entries.length === 0) return undefined;\n return {\n fileName,\n mimeType: \"application/x-ndjson\",\n content: entries.map((entry) => JSON.stringify(entry)).join(\"\\n\"),\n };\n}\n\nexport class ConveyorIncidents {\n private readonly apiKey: string;\n private readonly projectId: string;\n private readonly baseUrl: string;\n private readonly fetchImpl: typeof fetch;\n\n constructor(opts: ConveyorIncidentsOptions) {\n if (!opts.apiKey) {\n throw new Error(\"ConveyorIncidents: apiKey is required\");\n }\n if (!opts.projectId) {\n throw new Error(\"ConveyorIncidents: projectId is required\");\n }\n const resolvedFetch = opts.fetch ?? globalThis.fetch;\n if (typeof resolvedFetch !== \"function\") {\n throw new Error(\"ConveyorIncidents: no global fetch available — pass opts.fetch on Node <18\");\n }\n this.apiKey = opts.apiKey;\n this.projectId = opts.projectId;\n this.baseUrl = stripTrailingSlash(opts.baseUrl ?? DEFAULT_BASE_URL);\n this.fetchImpl = resolvedFetch;\n }\n\n async report(input: ReportIncidentInput): Promise<ReportIncidentResult> {\n const body = {\n projectId: this.projectId,\n source: input.source ?? DEFAULT_SOURCE,\n severity: input.severity ?? DEFAULT_SEVERITY,\n title: input.title,\n description: input.description,\n plan: input.plan,\n fingerprintKey: input.fingerprintKey,\n type: input.type,\n assigned: input.assigned,\n review: input.review,\n createdBy: input.createdBy,\n reporter: normalizeReporter(input.reporter),\n };\n\n const res = await this.fetchImpl(`${this.baseUrl}/api/incidents/report`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify(body),\n });\n\n const payload = await readJson(res);\n\n if (!res.ok) {\n throw new ConveyorIncidentsError(\n `Failed to report incident: ${res.status} ${res.statusText}`,\n res.status,\n payload,\n );\n }\n\n const incidentId = extractId(payload, \"incidentId\") ?? extractId(payload, \"cardId\");\n\n if (!incidentId) {\n throw new ConveyorIncidentsError(\n \"Invalid response from /api/incidents/report: missing incidentId\",\n res.status,\n payload,\n );\n }\n\n const attachmentResults: AttachmentResult[] = [];\n if (input.attachments && input.attachments.length > 0) {\n for (const attachment of input.attachments) {\n attachmentResults.push(await this.uploadAttachment(incidentId, attachment));\n }\n }\n\n return { incidentId, cardId: incidentId, attachmentResults };\n }\n\n private async uploadAttachment(\n incidentId: string,\n attachment: IncidentAttachment,\n ): Promise<AttachmentResult> {\n try {\n const bytes =\n typeof attachment.content === \"string\"\n ? new TextEncoder().encode(attachment.content)\n : attachment.content;\n if (bytes.byteLength > MAX_ATTACHMENT_BYTES) {\n return {\n fileName: attachment.fileName,\n ok: false,\n error: `Attachment is ${bytes.byteLength} bytes — exceeds the ${MAX_ATTACHMENT_BYTES}-byte (5 MB) server limit; upload skipped`,\n };\n }\n const contentBase64 = bytesToBase64(bytes);\n const res = await this.fetchImpl(\n `${this.baseUrl}/api/incidents/${encodeURIComponent(incidentId)}/files`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify({\n fileName: attachment.fileName,\n mimeType: attachment.mimeType,\n contentBase64,\n }),\n },\n );\n\n const payload = await readJson(res);\n\n if (!res.ok) {\n return {\n fileName: attachment.fileName,\n ok: false,\n error: formatErrorBody(res.status, res.statusText, payload),\n };\n }\n\n const fileId =\n payload && typeof payload === \"object\" && \"fileId\" in payload\n ? String((payload as { fileId: unknown }).fileId)\n : undefined;\n\n return { fileName: attachment.fileName, ok: true, fileId };\n } catch (err) {\n return {\n fileName: attachment.fileName,\n ok: false,\n error: err instanceof Error ? err.message : String(err),\n };\n }\n }\n}\n\nfunction pushNetworkEntry(entry: NetworkLogEntry): void {\n const state = networkCaptureState;\n if (!state) return;\n state.entries.push(entry);\n const overflow = state.entries.length - state.options.maxEntries;\n if (overflow > 0) state.entries.splice(0, overflow);\n}\n\nfunction normalizeRequest(\n input: RequestInfo | URL,\n init: RequestInit | undefined,\n): { method: string; url: string; traceparent?: string } {\n const request = typeof Request !== \"undefined\" && input instanceof Request ? input : undefined;\n const method = init?.method ?? request?.method ?? \"GET\";\n const url = request?.url ?? String(input);\n const traceparent =\n readHeader(init?.headers, \"traceparent\") ?? readHeader(request?.headers, \"traceparent\");\n return { method: method.toUpperCase(), url, traceparent };\n}\n\nasync function buildNetworkEntry(params: {\n timestamp: string;\n request: { method: string; url: string; traceparent?: string };\n response: Response;\n durationMs: number;\n options: Required<NetworkCaptureOptions>;\n}): Promise<NetworkLogEntry> {\n const { timestamp, request, response, durationMs, options } = params;\n const contentType = response.headers.get(\"content-type\") ?? undefined;\n const trpcProcedures = extractTrpcProcedures(request.url);\n const shouldCaptureBody =\n !response.ok || (options.captureTrpcResponses && trpcProcedures !== undefined);\n const bodyText = shouldCaptureBody\n ? await readResponseSnippet(response, options.maxBodyChars)\n : undefined;\n const responseBytes = readContentLength(response.headers) ?? estimateUtf8Bytes(bodyText);\n\n return {\n timestamp,\n kind: \"fetch\",\n method: request.method,\n url: redactUrl(request.url),\n status: response.status,\n ok: response.ok,\n durationMs,\n responseBytes,\n contentType,\n requestTraceparent: request.traceparent,\n responseTraceparent: response.headers.get(\"traceparent\") ?? undefined,\n trpcProcedures,\n responseSnippet: bodyText,\n };\n}\n\nfunction readHeader(headers: HeadersInit | undefined, name: string): string | undefined {\n if (!headers) return undefined;\n if (headers instanceof Headers) return headers.get(name) ?? undefined;\n const lowerName = name.toLowerCase();\n if (Array.isArray(headers)) {\n const hit = headers.find(([key]) => key.toLowerCase() === lowerName);\n return hit?.[1];\n }\n const value = Object.entries(headers).find(([key]) => key.toLowerCase() === lowerName)?.[1];\n return value;\n}\n\nfunction readContentLength(headers: Headers): number | null {\n const raw = headers.get(\"content-length\");\n if (!raw) return null;\n const parsed = Number.parseInt(raw, 10);\n return Number.isFinite(parsed) ? parsed : null;\n}\n\nfunction estimateUtf8Bytes(value: string | undefined): number | null {\n if (value === undefined) return null;\n return new TextEncoder().encode(value).byteLength;\n}\n\nasync function readResponseSnippet(\n response: Response,\n maxChars: number,\n): Promise<string | undefined> {\n try {\n const text = await response.clone().text();\n if (text.length <= maxChars) return text;\n return `${text.slice(0, maxChars)}...[truncated ${String(text.length - maxChars)} chars]`;\n } catch {\n return undefined;\n }\n}\n\nfunction extractTrpcProcedures(rawUrl: string): string[] | undefined {\n let url: URL;\n try {\n url = new URL(rawUrl, \"https://placeholder.invalid\");\n } catch {\n return undefined;\n }\n const marker = \"/trpc/\";\n const index = url.pathname.indexOf(marker);\n if (index === -1) return undefined;\n const path = url.pathname.slice(index + marker.length);\n const procedures = path\n .split(\",\")\n .map((part) => decodeURIComponent(part).trim())\n .filter((part) => part.length > 0);\n return procedures.length > 0 ? procedures : undefined;\n}\n\nfunction redactUrl(rawUrl: string): string {\n let url: URL;\n try {\n url = new URL(rawUrl, \"https://placeholder.invalid\");\n } catch {\n return rawUrl;\n }\n for (const key of [...url.searchParams.keys()]) {\n const lower = key.toLowerCase();\n if (\n lower.includes(\"token\") ||\n lower.includes(\"key\") ||\n lower.includes(\"secret\") ||\n lower.includes(\"signature\") ||\n lower.includes(\"sig\")\n ) {\n url.searchParams.set(key, \"[redacted]\");\n }\n }\n return url.origin === \"https://placeholder.invalid\"\n ? `${url.pathname}${url.search}`\n : url.toString();\n}\n\nfunction stripTrailingSlash(url: string): string {\n return url.endsWith(\"/\") ? url.slice(0, -1) : url;\n}\n\n/**\n * Trim reporter fields and drop empty ones, so a caller passing\n * `{ name: user.name ?? \"\" }` doesn't send a blank line to the server.\n * Returns undefined when nothing useful survives.\n */\nfunction normalizeReporter(reporter: ReportReporter | undefined): ReportReporter | undefined {\n if (!reporter) return undefined;\n const entries = ([\"name\", \"email\", \"url\"] as const)\n .map((key) => [key, reporter[key]?.trim()] as const)\n .filter((entry): entry is readonly [(typeof entry)[0], string] => !!entry[1]);\n if (entries.length === 0) return undefined;\n return Object.fromEntries(entries) as ReportReporter;\n}\n\nfunction extractId(payload: unknown, key: \"incidentId\" | \"cardId\"): string | undefined {\n if (payload && typeof payload === \"object\" && key in payload) {\n const value = (payload as Record<string, unknown>)[key];\n if (value !== undefined && value !== null) return String(value);\n }\n return undefined;\n}\n\nasync function readJson(res: Response): Promise<unknown> {\n const text = await res.text();\n if (!text) return undefined;\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n\nfunction formatErrorBody(status: number, statusText: string, body: unknown): string {\n const prefix = `${status} ${statusText}`.trim();\n if (body && typeof body === \"object\" && \"error\" in body) {\n return `${prefix}: ${String((body as { error: unknown }).error)}`;\n }\n if (typeof body === \"string\" && body.length > 0) {\n return `${prefix}: ${body}`;\n }\n return prefix;\n}\n\nfunction bytesToBase64(bytes: Uint8Array): string {\n if (typeof Buffer !== \"undefined\") {\n return Buffer.from(bytes).toString(\"base64\");\n }\n let binary = \"\";\n for (const byte of bytes) {\n binary += String.fromCharCode(byte);\n }\n return btoa(binary);\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rallycry/conveyor-incidents",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Typed TypeScript client for Conveyor's public incidents REST API",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"client",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"prepublishOnly": "tsup",
|
|
24
24
|
"build": "tsup",
|
|
25
25
|
"build:js": "tsup --no-dts --no-clean",
|
|
26
|
-
"dev": "tsup --watch",
|
|
26
|
+
"dev": "tsup --watch --no-dts --no-clean",
|
|
27
27
|
"test": "vitest run --passWithNoTests",
|
|
28
28
|
"test:unit": "vitest run --passWithNoTests",
|
|
29
29
|
"test:watch": "vitest",
|