@rallycry/conveyor-incidents 0.3.1 → 0.3.2
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 +34 -0
- package/dist/index.d.ts +30 -1
- package/dist/index.js +181 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -100,6 +100,29 @@ Conveyor deduplicates reports by a fingerprint derived from `(projectId, source,
|
|
|
100
100
|
- **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
101
|
- **10 attachments per card** (server-enforced).
|
|
102
102
|
|
|
103
|
+
### Browser network evidence
|
|
104
|
+
|
|
105
|
+
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:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import {
|
|
109
|
+
buildNetworkLogAttachment,
|
|
110
|
+
installNetworkCapture,
|
|
111
|
+
} from "@rallycry/conveyor-incidents";
|
|
112
|
+
|
|
113
|
+
installNetworkCapture({ maxEntries: 120, maxBodyChars: 6000 });
|
|
114
|
+
|
|
115
|
+
await incidents.report({
|
|
116
|
+
title: "Competition page showed 403",
|
|
117
|
+
attachments: [
|
|
118
|
+
buildNetworkLogAttachment(),
|
|
119
|
+
// ...context.json, screenshot.jpg, console.log, etc.
|
|
120
|
+
].filter((it): it is NonNullable<typeof it> => it !== undefined),
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`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.
|
|
125
|
+
|
|
103
126
|
### Error handling
|
|
104
127
|
|
|
105
128
|
- The main `report()` call throws `ConveyorIncidentsError` on non-2xx responses; it exposes `.status` and `.body`.
|
|
@@ -153,6 +176,17 @@ client.report({
|
|
|
153
176
|
error?: string;
|
|
154
177
|
}>;
|
|
155
178
|
}>;
|
|
179
|
+
|
|
180
|
+
installNetworkCapture({
|
|
181
|
+
maxEntries?: number; // default: 80
|
|
182
|
+
maxBodyChars?: number; // default: 4000
|
|
183
|
+
captureTrpcResponses?: boolean; // default: true
|
|
184
|
+
}): void;
|
|
185
|
+
|
|
186
|
+
getNetworkLogEntries(): NetworkLogEntry[];
|
|
187
|
+
clearNetworkLogEntries(): void;
|
|
188
|
+
uninstallNetworkCapture(): void;
|
|
189
|
+
buildNetworkLogAttachment(fileName?: string): IncidentAttachment | undefined;
|
|
156
190
|
```
|
|
157
191
|
|
|
158
192
|
## License
|
package/dist/index.d.ts
CHANGED
|
@@ -21,6 +21,30 @@ 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
|
+
}
|
|
24
48
|
interface ReportIncidentInput {
|
|
25
49
|
title: string;
|
|
26
50
|
description?: string;
|
|
@@ -62,6 +86,11 @@ interface ReportIncidentResult {
|
|
|
62
86
|
cardId: string;
|
|
63
87
|
attachmentResults: AttachmentResult[];
|
|
64
88
|
}
|
|
89
|
+
declare function installNetworkCapture(options?: NetworkCaptureOptions): void;
|
|
90
|
+
declare function uninstallNetworkCapture(): void;
|
|
91
|
+
declare function clearNetworkLogEntries(): void;
|
|
92
|
+
declare function getNetworkLogEntries(): NetworkLogEntry[];
|
|
93
|
+
declare function buildNetworkLogAttachment(fileName?: string): IncidentAttachment | undefined;
|
|
65
94
|
declare class ConveyorIncidents {
|
|
66
95
|
private readonly apiKey;
|
|
67
96
|
private readonly projectId;
|
|
@@ -72,4 +101,4 @@ declare class ConveyorIncidents {
|
|
|
72
101
|
private uploadAttachment;
|
|
73
102
|
}
|
|
74
103
|
|
|
75
|
-
export { type AttachmentResult, type ConveyorCardType, ConveyorIncidents, ConveyorIncidentsError, type ConveyorIncidentsOptions, type IncidentAttachment, type IncidentSeverity, MAX_ATTACHMENT_BYTES, type ReportIncidentInput, type ReportIncidentResult };
|
|
104
|
+
export { type AttachmentResult, type ConveyorCardType, ConveyorIncidents, ConveyorIncidentsError, type ConveyorIncidentsOptions, type IncidentAttachment, type IncidentSeverity, MAX_ATTACHMENT_BYTES, type NetworkCaptureOptions, type NetworkLogEntry, type ReportIncidentInput, type ReportIncidentResult, 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;
|
|
@@ -127,6 +206,102 @@ var ConveyorIncidents = class {
|
|
|
127
206
|
}
|
|
128
207
|
}
|
|
129
208
|
};
|
|
209
|
+
function pushNetworkEntry(entry) {
|
|
210
|
+
const state = networkCaptureState;
|
|
211
|
+
if (!state) return;
|
|
212
|
+
state.entries.push(entry);
|
|
213
|
+
const overflow = state.entries.length - state.options.maxEntries;
|
|
214
|
+
if (overflow > 0) state.entries.splice(0, overflow);
|
|
215
|
+
}
|
|
216
|
+
function normalizeRequest(input, init) {
|
|
217
|
+
const request = typeof Request !== "undefined" && input instanceof Request ? input : void 0;
|
|
218
|
+
const method = init?.method ?? request?.method ?? "GET";
|
|
219
|
+
const url = request?.url ?? String(input);
|
|
220
|
+
const traceparent = readHeader(init?.headers, "traceparent") ?? readHeader(request?.headers, "traceparent");
|
|
221
|
+
return { method: method.toUpperCase(), url, traceparent };
|
|
222
|
+
}
|
|
223
|
+
async function buildNetworkEntry(params) {
|
|
224
|
+
const { timestamp, request, response, durationMs, options } = params;
|
|
225
|
+
const contentType = response.headers.get("content-type") ?? void 0;
|
|
226
|
+
const trpcProcedures = extractTrpcProcedures(request.url);
|
|
227
|
+
const shouldCaptureBody = !response.ok || options.captureTrpcResponses && trpcProcedures !== void 0;
|
|
228
|
+
const bodyText = shouldCaptureBody ? await readResponseSnippet(response, options.maxBodyChars) : void 0;
|
|
229
|
+
const responseBytes = readContentLength(response.headers) ?? estimateUtf8Bytes(bodyText);
|
|
230
|
+
return {
|
|
231
|
+
timestamp,
|
|
232
|
+
kind: "fetch",
|
|
233
|
+
method: request.method,
|
|
234
|
+
url: redactUrl(request.url),
|
|
235
|
+
status: response.status,
|
|
236
|
+
ok: response.ok,
|
|
237
|
+
durationMs,
|
|
238
|
+
responseBytes,
|
|
239
|
+
contentType,
|
|
240
|
+
requestTraceparent: request.traceparent,
|
|
241
|
+
responseTraceparent: response.headers.get("traceparent") ?? void 0,
|
|
242
|
+
trpcProcedures,
|
|
243
|
+
responseSnippet: bodyText
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function readHeader(headers, name) {
|
|
247
|
+
if (!headers) return void 0;
|
|
248
|
+
if (headers instanceof Headers) return headers.get(name) ?? void 0;
|
|
249
|
+
const lowerName = name.toLowerCase();
|
|
250
|
+
if (Array.isArray(headers)) {
|
|
251
|
+
const hit = headers.find(([key]) => key.toLowerCase() === lowerName);
|
|
252
|
+
return hit?.[1];
|
|
253
|
+
}
|
|
254
|
+
const value = Object.entries(headers).find(([key]) => key.toLowerCase() === lowerName)?.[1];
|
|
255
|
+
return value;
|
|
256
|
+
}
|
|
257
|
+
function readContentLength(headers) {
|
|
258
|
+
const raw = headers.get("content-length");
|
|
259
|
+
if (!raw) return null;
|
|
260
|
+
const parsed = Number.parseInt(raw, 10);
|
|
261
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
262
|
+
}
|
|
263
|
+
function estimateUtf8Bytes(value) {
|
|
264
|
+
if (value === void 0) return null;
|
|
265
|
+
return new TextEncoder().encode(value).byteLength;
|
|
266
|
+
}
|
|
267
|
+
async function readResponseSnippet(response, maxChars) {
|
|
268
|
+
try {
|
|
269
|
+
const text = await response.clone().text();
|
|
270
|
+
if (text.length <= maxChars) return text;
|
|
271
|
+
return `${text.slice(0, maxChars)}...[truncated ${String(text.length - maxChars)} chars]`;
|
|
272
|
+
} catch {
|
|
273
|
+
return void 0;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function extractTrpcProcedures(rawUrl) {
|
|
277
|
+
let url;
|
|
278
|
+
try {
|
|
279
|
+
url = new URL(rawUrl, "https://placeholder.invalid");
|
|
280
|
+
} catch {
|
|
281
|
+
return void 0;
|
|
282
|
+
}
|
|
283
|
+
const marker = "/trpc/";
|
|
284
|
+
const index = url.pathname.indexOf(marker);
|
|
285
|
+
if (index === -1) return void 0;
|
|
286
|
+
const path = url.pathname.slice(index + marker.length);
|
|
287
|
+
const procedures = path.split(",").map((part) => decodeURIComponent(part).trim()).filter((part) => part.length > 0);
|
|
288
|
+
return procedures.length > 0 ? procedures : void 0;
|
|
289
|
+
}
|
|
290
|
+
function redactUrl(rawUrl) {
|
|
291
|
+
let url;
|
|
292
|
+
try {
|
|
293
|
+
url = new URL(rawUrl, "https://placeholder.invalid");
|
|
294
|
+
} catch {
|
|
295
|
+
return rawUrl;
|
|
296
|
+
}
|
|
297
|
+
for (const key of [...url.searchParams.keys()]) {
|
|
298
|
+
const lower = key.toLowerCase();
|
|
299
|
+
if (lower.includes("token") || lower.includes("key") || lower.includes("secret") || lower.includes("signature") || lower.includes("sig")) {
|
|
300
|
+
url.searchParams.set(key, "[redacted]");
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return url.origin === "https://placeholder.invalid" ? `${url.pathname}${url.search}` : url.toString();
|
|
304
|
+
}
|
|
130
305
|
function stripTrailingSlash(url) {
|
|
131
306
|
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
132
307
|
}
|
|
@@ -169,6 +344,11 @@ function bytesToBase64(bytes) {
|
|
|
169
344
|
export {
|
|
170
345
|
ConveyorIncidents,
|
|
171
346
|
ConveyorIncidentsError,
|
|
172
|
-
MAX_ATTACHMENT_BYTES
|
|
347
|
+
MAX_ATTACHMENT_BYTES,
|
|
348
|
+
buildNetworkLogAttachment,
|
|
349
|
+
clearNetworkLogEntries,
|
|
350
|
+
getNetworkLogEntries,
|
|
351
|
+
installNetworkCapture,
|
|
352
|
+
uninstallNetworkCapture
|
|
173
353
|
};
|
|
174
354
|
//# 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\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\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 };\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\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.2",
|
|
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",
|