crashrelay 0.1.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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +150 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +79 -0
  5. package/dist/collectors/httpErrors.d.ts +21 -0
  6. package/dist/collectors/httpErrors.js +53 -0
  7. package/dist/collectors/logTail.d.ts +27 -0
  8. package/dist/collectors/logTail.js +86 -0
  9. package/dist/collectors/processCrash.d.ts +24 -0
  10. package/dist/collectors/processCrash.js +41 -0
  11. package/dist/config.d.ts +10 -0
  12. package/dist/config.js +69 -0
  13. package/dist/daemon.d.ts +13 -0
  14. package/dist/daemon.js +37 -0
  15. package/dist/dedup.d.ts +18 -0
  16. package/dist/dedup.js +72 -0
  17. package/dist/fetcher.d.ts +10 -0
  18. package/dist/fetcher.js +5 -0
  19. package/dist/fingerprint.d.ts +7 -0
  20. package/dist/fingerprint.js +57 -0
  21. package/dist/index.d.ts +16 -0
  22. package/dist/index.js +42 -0
  23. package/dist/ingestion/auth.d.ts +7 -0
  24. package/dist/ingestion/auth.js +21 -0
  25. package/dist/ingestion/cors.d.ts +7 -0
  26. package/dist/ingestion/cors.js +23 -0
  27. package/dist/ingestion/rateLimit.d.ts +11 -0
  28. package/dist/ingestion/rateLimit.js +24 -0
  29. package/dist/ingestion/server.d.ts +15 -0
  30. package/dist/ingestion/server.js +84 -0
  31. package/dist/init.d.ts +30 -0
  32. package/dist/init.js +58 -0
  33. package/dist/pipeline.d.ts +14 -0
  34. package/dist/pipeline.js +53 -0
  35. package/dist/providers/adf.d.ts +24 -0
  36. package/dist/providers/adf.js +28 -0
  37. package/dist/providers/format.d.ts +3 -0
  38. package/dist/providers/format.js +23 -0
  39. package/dist/providers/github.d.ts +4 -0
  40. package/dist/providers/github.js +47 -0
  41. package/dist/providers/index.d.ts +7 -0
  42. package/dist/providers/index.js +19 -0
  43. package/dist/providers/jira.d.ts +4 -0
  44. package/dist/providers/jira.js +53 -0
  45. package/dist/providers/types.d.ts +8 -0
  46. package/dist/providers/types.js +2 -0
  47. package/dist/types.d.ts +62 -0
  48. package/dist/types.js +2 -0
  49. package/package.json +41 -0
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildSummary = buildSummary;
4
+ exports.buildBody = buildBody;
5
+ const MAX_SUMMARY_LENGTH = 120;
6
+ function buildSummary(defect) {
7
+ const firstLine = defect.message.split('\n')[0].trim();
8
+ const summary = `[${defect.type}] ${firstLine}`;
9
+ return summary.length > MAX_SUMMARY_LENGTH ? summary.slice(0, MAX_SUMMARY_LENGTH - 1) + '…' : summary;
10
+ }
11
+ function buildBody(defect) {
12
+ const lines = [`Type: ${defect.type}`, `First seen: ${defect.occurredAt}`];
13
+ if (defect.context) {
14
+ for (const [key, value] of Object.entries(defect.context)) {
15
+ if (value !== undefined)
16
+ lines.push(`${key}: ${value}`);
17
+ }
18
+ }
19
+ lines.push('', defect.message);
20
+ if (defect.stack)
21
+ lines.push('', defect.stack);
22
+ return lines.join('\n');
23
+ }
@@ -0,0 +1,4 @@
1
+ import type { Fetcher } from '../fetcher';
2
+ import type { GithubConfig } from '../types';
3
+ import type { TicketProvider } from './types';
4
+ export declare function createGithubProvider(config: GithubConfig, fetcher: Fetcher): TicketProvider;
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createGithubProvider = createGithubProvider;
4
+ const format_1 = require("./format");
5
+ async function githubFetch(config, fetcher, path, init) {
6
+ const response = await fetcher(`https://api.github.com/repos/${config.owner}/${config.repo}${path}`, {
7
+ method: init?.method ?? 'GET',
8
+ headers: {
9
+ Authorization: `Bearer ${config.token}`,
10
+ Accept: 'application/vnd.github+json',
11
+ 'X-GitHub-Api-Version': '2022-11-28',
12
+ 'Content-Type': 'application/json',
13
+ },
14
+ body: init?.body !== undefined ? JSON.stringify(init.body) : undefined,
15
+ });
16
+ if (!response.ok) {
17
+ throw new Error(`GitHub API error (HTTP ${response.status}) for ${path}`);
18
+ }
19
+ return response;
20
+ }
21
+ function createGithubProvider(config, fetcher) {
22
+ return {
23
+ name: 'github',
24
+ async createTicket(defect) {
25
+ const response = await githubFetch(config, fetcher, '/issues', {
26
+ method: 'POST',
27
+ body: {
28
+ title: (0, format_1.buildSummary)(defect),
29
+ body: '```\n' + (0, format_1.buildBody)(defect) + '\n```',
30
+ labels: ['crashrelay', 'auto-filed'],
31
+ },
32
+ });
33
+ const body = (await response.json());
34
+ return { provider: 'github', id: `#${body.number}`, url: body.html_url };
35
+ },
36
+ async addComment(ticket, text) {
37
+ const number = ticket.id.replace(/^#/, '');
38
+ await githubFetch(config, fetcher, `/issues/${number}/comments`, {
39
+ method: 'POST',
40
+ body: { body: text },
41
+ });
42
+ },
43
+ async checkConnection() {
44
+ await githubFetch(config, fetcher, '');
45
+ },
46
+ };
47
+ }
@@ -0,0 +1,7 @@
1
+ import type { Fetcher } from '../fetcher';
2
+ import type { Config } from '../types';
3
+ import type { TicketProvider } from './types';
4
+ export declare function resolveProviders(config: Config, fetcher: Fetcher): TicketProvider[];
5
+ export type { TicketProvider } from './types';
6
+ export { textToAdf } from './adf';
7
+ export { buildSummary, buildBody } from './format';
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildBody = exports.buildSummary = exports.textToAdf = void 0;
4
+ exports.resolveProviders = resolveProviders;
5
+ const jira_1 = require("./jira");
6
+ const github_1 = require("./github");
7
+ function resolveProviders(config, fetcher) {
8
+ const providers = [];
9
+ if (config.jira)
10
+ providers.push((0, jira_1.createJiraProvider)(config.jira, fetcher));
11
+ if (config.github)
12
+ providers.push((0, github_1.createGithubProvider)(config.github, fetcher));
13
+ return providers;
14
+ }
15
+ var adf_1 = require("./adf");
16
+ Object.defineProperty(exports, "textToAdf", { enumerable: true, get: function () { return adf_1.textToAdf; } });
17
+ var format_1 = require("./format");
18
+ Object.defineProperty(exports, "buildSummary", { enumerable: true, get: function () { return format_1.buildSummary; } });
19
+ Object.defineProperty(exports, "buildBody", { enumerable: true, get: function () { return format_1.buildBody; } });
@@ -0,0 +1,4 @@
1
+ import type { Fetcher } from '../fetcher';
2
+ import type { JiraConfig } from '../types';
3
+ import type { TicketProvider } from './types';
4
+ export declare function createJiraProvider(config: JiraConfig, fetcher: Fetcher): TicketProvider;
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createJiraProvider = createJiraProvider;
4
+ const adf_1 = require("./adf");
5
+ const format_1 = require("./format");
6
+ function authHeader(config) {
7
+ const encoded = Buffer.from(`${config.email}:${config.apiToken}`).toString('base64');
8
+ return `Basic ${encoded}`;
9
+ }
10
+ async function jiraFetch(config, fetcher, path, init) {
11
+ const response = await fetcher(`${config.baseUrl}${path}`, {
12
+ method: init?.method ?? 'GET',
13
+ headers: {
14
+ Authorization: authHeader(config),
15
+ 'Content-Type': 'application/json',
16
+ Accept: 'application/json',
17
+ },
18
+ body: init?.body !== undefined ? JSON.stringify(init.body) : undefined,
19
+ });
20
+ if (!response.ok) {
21
+ throw new Error(`Jira API error (HTTP ${response.status}) for ${path}`);
22
+ }
23
+ return response;
24
+ }
25
+ function createJiraProvider(config, fetcher) {
26
+ return {
27
+ name: 'jira',
28
+ async createTicket(defect) {
29
+ const response = await jiraFetch(config, fetcher, '/rest/api/3/issue', {
30
+ method: 'POST',
31
+ body: {
32
+ fields: {
33
+ project: { key: config.projectKey },
34
+ summary: (0, format_1.buildSummary)(defect),
35
+ issuetype: { name: config.issueType },
36
+ description: (0, adf_1.textToAdf)((0, format_1.buildBody)(defect)),
37
+ },
38
+ },
39
+ });
40
+ const body = (await response.json());
41
+ return { provider: 'jira', id: body.key, url: `${config.baseUrl}/browse/${body.key}` };
42
+ },
43
+ async addComment(ticket, text) {
44
+ await jiraFetch(config, fetcher, `/rest/api/3/issue/${ticket.id}/comment`, {
45
+ method: 'POST',
46
+ body: { body: (0, adf_1.textToAdf)(text) },
47
+ });
48
+ },
49
+ async checkConnection() {
50
+ await jiraFetch(config, fetcher, `/rest/api/3/project/${config.projectKey}`);
51
+ },
52
+ };
53
+ }
@@ -0,0 +1,8 @@
1
+ import type { Defect, TicketRef } from '../types';
2
+ export interface TicketProvider {
3
+ name: string;
4
+ createTicket(defect: Defect): Promise<TicketRef>;
5
+ addComment(ticket: TicketRef, text: string): Promise<void>;
6
+ /** Lightweight read-only call used by `crashrelay test-ticket --dry-run` to verify credentials without creating anything. */
7
+ checkConnection(): Promise<void>;
8
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,62 @@
1
+ export type DefectType = 'process-crash' | 'unhandled-rejection' | 'http-5xx' | 'log-error' | 'client-error';
2
+ export interface Defect {
3
+ type: DefectType;
4
+ message: string;
5
+ stack?: string;
6
+ /** Extra context: route, status code, log line, source URL, etc. */
7
+ context?: Record<string, string | number | undefined>;
8
+ occurredAt: string;
9
+ }
10
+ export interface TicketRef {
11
+ provider: string;
12
+ id: string;
13
+ url: string;
14
+ }
15
+ export interface JiraConfig {
16
+ baseUrl: string;
17
+ email: string;
18
+ apiToken: string;
19
+ projectKey: string;
20
+ issueType: string;
21
+ }
22
+ export interface GithubConfig {
23
+ token: string;
24
+ owner: string;
25
+ repo: string;
26
+ }
27
+ export interface IngestionConfig {
28
+ enabled: boolean;
29
+ port: number;
30
+ token: string;
31
+ allowedOrigins: string[];
32
+ }
33
+ export interface Config {
34
+ jira?: JiraConfig;
35
+ github?: GithubConfig;
36
+ ingestion?: IngestionConfig;
37
+ dedupCooldownHours: number;
38
+ commentCooldownMinutes: number;
39
+ logTailPath?: string;
40
+ logTailPattern: string;
41
+ cacheFilePath: string;
42
+ }
43
+ export interface DedupCacheEntry {
44
+ firstSeenAt: string;
45
+ lastSeenAt: string;
46
+ count: number;
47
+ cooldownUntil: string;
48
+ lastCommentAt?: string;
49
+ ticket: TicketRef;
50
+ }
51
+ export interface DedupCache {
52
+ entries: Record<string, DedupCacheEntry>;
53
+ }
54
+ export type DedupDecision = {
55
+ action: 'create';
56
+ } | {
57
+ action: 'comment';
58
+ entry: DedupCacheEntry;
59
+ } | {
60
+ action: 'skip';
61
+ entry: DedupCacheEntry;
62
+ };
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "crashrelay",
3
+ "version": "0.1.0",
4
+ "description": "Catch backend crashes, HTTP 5xx responses, and error-log lines (plus frontend JS errors via crashrelay-browser) and auto-file tickets in Jira or GitHub Issues.",
5
+ "license": "MIT",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "crashrelay": "dist/cli.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "README.md"
14
+ ],
15
+ "engines": {
16
+ "node": ">=20"
17
+ },
18
+ "scripts": {
19
+ "build": "tsc -p tsconfig.json",
20
+ "test": "tsc -p tsconfig.test.json && find .test-build -name '*.test.js' -print0 | xargs -0 node --test",
21
+ "prepublishOnly": "npm run build"
22
+ },
23
+ "keywords": [
24
+ "error-tracking",
25
+ "jira",
26
+ "github-issues",
27
+ "monitoring",
28
+ "crash-reporting",
29
+ "cli",
30
+ "ticketing"
31
+ ],
32
+ "dependencies": {
33
+ "@inquirer/prompts": "^8.5.2",
34
+ "commander": "^12.1.0",
35
+ "node-cron": "^4.6.0"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^20.14.0",
39
+ "typescript": "^5.6.0"
40
+ }
41
+ }