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.
- package/LICENSE +21 -0
- package/README.md +150 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +79 -0
- package/dist/collectors/httpErrors.d.ts +21 -0
- package/dist/collectors/httpErrors.js +53 -0
- package/dist/collectors/logTail.d.ts +27 -0
- package/dist/collectors/logTail.js +86 -0
- package/dist/collectors/processCrash.d.ts +24 -0
- package/dist/collectors/processCrash.js +41 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.js +69 -0
- package/dist/daemon.d.ts +13 -0
- package/dist/daemon.js +37 -0
- package/dist/dedup.d.ts +18 -0
- package/dist/dedup.js +72 -0
- package/dist/fetcher.d.ts +10 -0
- package/dist/fetcher.js +5 -0
- package/dist/fingerprint.d.ts +7 -0
- package/dist/fingerprint.js +57 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +42 -0
- package/dist/ingestion/auth.d.ts +7 -0
- package/dist/ingestion/auth.js +21 -0
- package/dist/ingestion/cors.d.ts +7 -0
- package/dist/ingestion/cors.js +23 -0
- package/dist/ingestion/rateLimit.d.ts +11 -0
- package/dist/ingestion/rateLimit.js +24 -0
- package/dist/ingestion/server.d.ts +15 -0
- package/dist/ingestion/server.js +84 -0
- package/dist/init.d.ts +30 -0
- package/dist/init.js +58 -0
- package/dist/pipeline.d.ts +14 -0
- package/dist/pipeline.js +53 -0
- package/dist/providers/adf.d.ts +24 -0
- package/dist/providers/adf.js +28 -0
- package/dist/providers/format.d.ts +3 -0
- package/dist/providers/format.js +23 -0
- package/dist/providers/github.d.ts +4 -0
- package/dist/providers/github.js +47 -0
- package/dist/providers/index.d.ts +7 -0
- package/dist/providers/index.js +19 -0
- package/dist/providers/jira.d.ts +4 -0
- package/dist/providers/jira.js +53 -0
- package/dist/providers/types.d.ts +8 -0
- package/dist/providers/types.js +2 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.js +2 -0
- 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,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,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
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -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
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
|
+
}
|