@rallycry/conveyor-incidents
v0.3.8
Published
Typed TypeScript client for Conveyor's public incidents REST API
Maintainers
Readme
@rallycry/conveyor-incidents
Typed TypeScript client for reporting incidents to Conveyor via the public REST API.
Install
npm install @rallycry/conveyor-incidents
# or
bun add @rallycry/conveyor-incidentsUsage
import { ConveyorIncidents } from "@rallycry/conveyor-incidents";
const incidents = new ConveyorIncidents({
apiKey: process.env.CONVEYOR_INCIDENT_TOKEN!, // cvyr_... project token with "incident" scope
projectId: process.env.CONVEYOR_PROJECT_ID!,
});
const { incidentId, attachmentResults } = await incidents.report({
title: "Checkout service returned 500",
description: "Happened on release v1.4.2 under load.",
severity: "high",
source: "prod-web", // stable values let the server dedup repeats
fingerprintKey: "checkout-500-v1.4.2",
attachments: [
{
fileName: "stack.txt",
mimeType: "text/plain",
content: stackTrace, // string or Uint8Array
},
],
});Card types
report() accepts an optional type — "incident" (default), "suggestion", or "task":
"incident"— fingerprint dedup (see below); repeat reports collapse into one incident."suggestion"— AI duplicate detection; a repeat report upvotes the existing suggestion and returns its id instead of creating a new card.fingerprintKeyandseverityare ignored (suggestions are ranked by votes)."task"— creates a plain task card. No dedup;fingerprintKeyis ignored.severitymaps to the card's priority.
// Platform-user suggestion, routed to the team for review
await incidents.report({
type: "suggestion",
title: "Add dark mode toggle",
assigned: ["[email protected]"],
review: ["[email protected]", "[email protected]"],
});Assigning members by email
assigned and review are optional arrays of email addresses, matched against the target project's members:
assigned— the first email that resolves to a project member becomes the card's assignee; remaining entries are ignored.review— every email that resolves to a project member is added as a reviewer.- Emails that don't match a project member are silently skipped — safe to pass end-user emails; only teammates who are members of the Conveyor project resolve.
- Applied only when a new card is created. Dedup hits (incident fingerprint match, suggestion AI-merge) leave the existing card untouched.
// Incident auto-assigned when the reporter is on your own email domain
await incidents.report({
title: "Checkout service returned 500",
severity: "high",
fingerprintKey: "checkout-500-v1.4.2",
assigned: ["[email protected]"],
});Creator attribution (createdBy)
createdBy is an optional single email, matched against the target project's members the same way as assigned/review:
- A resolvable project-member email becomes the card's creator.
- When omitted — or when the email doesn't resolve to a member — the card is attributed to the "Conveyor Incidents" system user, not a human.
- Applied only when a new card is created; dedup hits keep the existing creator.
Behavior change in 0.3.0: previously, cards reported through this endpoint were attributed to the token owner (suggestions/tasks) or the project owner (incidents). Pass
createdByexplicitly if you want a specific member credited.
await incidents.report({
type: "task",
title: "Wire up the new webhook",
createdBy: "[email protected]",
});Reporter metadata (reporter)
For any user-initiated report, send who filed it and what page they were on:
await incidents.report({
title: "Riot login fails",
description: userMessage,
reporter: {
name: currentUser.displayName,
email: currentUser.email,
url: window.location.href,
},
});Conveyor renders these as Reported by: / URL: lines in the card description's
--- metadata footer, and surfaces them on the card's Slack message. Unlike
createdBy, no project membership is required — this is descriptive metadata,
so an end-user report keeps an identity instead of reading as an anonymous
automated alert. Lines your own footer already writes (e.g. your own URL:) are
not duplicated, and url must be an absolute http(s) URL.
Conveyor also stores reporter.email on the card as a structured field. When a
project has reporter emails enabled, that address is the one Conveyor drafts a
thank-you reply to — so send it for user-initiated reports even when the reporter
has no Conveyor account. A malformed address is dropped; the report still lands.
About source and fingerprintKey
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.
Attachment limits
- 5 MB per file (
MAX_ATTACHMENT_BYTES, exported). Oversized files are rejected client-side with a descriptiveattachmentResultserror — no HTTP call is made. - 10 attachments per card (server-enforced).
Browser network evidence
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:
import {
buildNetworkLogAttachment,
installNetworkCapture,
} from "@rallycry/conveyor-incidents";
installNetworkCapture({ maxEntries: 120, maxBodyChars: 6000 });
await incidents.report({
title: "Competition page showed 403",
attachments: [
buildNetworkLogAttachment(),
// ...context.json, screenshot.jpg, console.log, etc.
].filter((it): it is NonNullable<typeof it> => it !== undefined),
});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.
Error handling
- The main
report()call throwsConveyorIncidentsErroron non-2xx responses; it exposes.statusand.body. - Attachment upload failures are captured per-file in
attachmentResultsand never throw — inspectokanderror.
Node <18
Pass a fetch polyfill via opts.fetch if your runtime lacks a global fetch.
DEP0169 url.parse() warning
The @rallycry/conveyor-incidents SDK does not call url.parse() — it uses the WHATWG URL constructor and the global fetch API. If your process emits this warning while using the SDK, the call is coming from another dependency in your project. Run node --trace-deprecation your-script.js to locate the source.
Troubleshooting 403 from /api/incidents/report
The endpoint requires a project token with scope: "incident". After the 2026-04-22 scope migration, older project tokens default to scope: "claude_code" and are rejected. The error body now includes a reason field — "scope_mismatch" indicates the token exists but is scoped for another surface; generate a new token from Project Settings → Incidents in the Conveyor UI.
API
new ConveyorIncidents({
apiKey: string;
projectId: string;
baseUrl?: string; // default: "https://api.conveyor.rallycryapp.com"
fetch?: typeof fetch;
});
client.report({
title: string;
description?: string;
severity?: "critical" | "high" | "medium" | "low"; // default: "medium"
source?: string; // default: "client"
fingerprintKey?: string;
plan?: string;
type?: "incident" | "suggestion" | "task"; // default: "incident"
assigned?: string[]; // project-member emails; first match becomes assignee
review?: string[]; // project-member emails; every match becomes a reviewer
createdBy?: string; // project-member email; no match → "Conveyor Incidents" system user
reporter?: { // who filed it + where; rendered into the card's metadata footer
name?: string;
email?: string; // also stored on the card; used for reporter thank-you emails
url?: string; // absolute http(s) URL of the page at report time
};
attachments?: Array<{
fileName: string;
mimeType: string;
content: Uint8Array | string; // strings are utf-8 encoded
}>;
}): Promise<{
incidentId: string;
cardId: string; // alias of incidentId — prefer for suggestion/task types
attachmentResults: Array<{
fileName: string;
ok: boolean;
fileId?: string;
error?: string;
}>;
}>;
installNetworkCapture({
maxEntries?: number; // default: 80
maxBodyChars?: number; // default: 4000
captureTrpcResponses?: boolean; // default: true
}): void;
getNetworkLogEntries(): NetworkLogEntry[];
clearNetworkLogEntries(): void;
uninstallNetworkCapture(): void;
buildNetworkLogAttachment(fileName?: string): IncidentAttachment | undefined;License
MIT
