gh-issue-tracker 1.0.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,193 @@
1
+ # gh-issue-tracker
2
+
3
+ Lightweight error tracking that creates GitHub Issues instead of sending to SaaS. Deduplication, fingerprinting, and rate limiting built-in.
4
+
5
+ ## Why
6
+
7
+ - **Zero SaaS cost** — errors go directly to GitHub Issues
8
+ - **Deduplication built-in** — same error creates one issue, not N duplicates
9
+ - **Fingerprinting** — stable error identity across deploys (line number changes don't matter)
10
+ - **Rate limiting** — prevents GitHub API spam during error storms
11
+ - **Simple API** — `init()` once, `captureException()` anywhere
12
+
13
+ ## How it works
14
+
15
+ ```
16
+ Error thrown
17
+ → Generate fingerprint (SHA-256 of name + message + top 3 normalized stack frames)
18
+ → Check rate limiter (sliding window + dedup)
19
+ → Search GitHub Issues by fingerprint label
20
+ → Open issue found? → Add thumbs-up reaction (count = frequency)
21
+ → Closed issue found? → Reopen + add comment
22
+ → No issue found? → Create new issue with error-report + fingerprint labels
23
+ ```
24
+
25
+ ## Quick start
26
+
27
+ ```bash
28
+ npm install gh-issue-tracker
29
+ ```
30
+
31
+ ```ts
32
+ import { init, captureException, flush } from 'gh-issue-tracker'
33
+
34
+ init({
35
+ githubToken: process.env.GITHUB_TOKEN!,
36
+ githubRepo: 'myorg/myapp',
37
+ environment: 'production',
38
+ })
39
+
40
+ try {
41
+ riskyOperation()
42
+ } catch (error) {
43
+ captureException(error instanceof Error ? error : new Error(String(error)))
44
+ await flush() // wait for GitHub API call (important in serverless)
45
+ }
46
+ ```
47
+
48
+ ## Configuration
49
+
50
+ | Option | Type | Default | Description |
51
+ |--------|------|---------|-------------|
52
+ | `githubToken` | `string` | — | **Required.** GitHub PAT with Issues read/write permission |
53
+ | `githubRepo` | `string` | — | **Required.** Repository in `owner/repo` format |
54
+ | `environment` | `string` | `"development"` | Environment name shown in issue body |
55
+ | `labels` | `string[]` | `[]` | Additional labels applied to every issue |
56
+ | `enabled` | `boolean` | `true` | Kill switch to disable tracking |
57
+ | `onError` | `(err) => void` | `console.error` | Called when the GitHub API fails |
58
+ | `rateLimitPerMinute` | `number` | `10` | Max new issues created per minute |
59
+ | `dedupeWindowMs` | `number` | `60000` | Suppress same fingerprint within this window (ms) |
60
+ | `reopenClosed` | `boolean` | `true` | Reopen closed issues on error recurrence |
61
+
62
+ ## API
63
+
64
+ ### `init(config: ErrorTrackerConfig): void`
65
+
66
+ Initialize the error tracker. Call once at app startup. Must be called before `captureException` or `captureMessage`.
67
+
68
+ ### `captureException(error: Error, context?: ErrorContext): void`
69
+
70
+ Capture an exception. Fire-and-forget — the GitHub API call happens in the background.
71
+
72
+ ```ts
73
+ captureException(error, {
74
+ tags: { component: 'auth', severity: 'critical' },
75
+ extras: { userId: '123', action: 'login' },
76
+ user: { id: '123', email: 'user@example.com' },
77
+ requestUrl: '/api/login',
78
+ })
79
+ ```
80
+
81
+ ### `captureMessage(message: string, level?: 'error' | 'warning', context?: ErrorContext): void`
82
+
83
+ Capture a plain message as an error event.
84
+
85
+ ```ts
86
+ captureMessage('Payment processing timeout', 'warning', {
87
+ tags: { provider: 'stripe' },
88
+ })
89
+ ```
90
+
91
+ ### `flush(): Promise<void>`
92
+
93
+ Wait for all pending error reports to complete. **Always call before serverless functions return.**
94
+
95
+ ```ts
96
+ captureException(error)
97
+ await flush() // don't return until the GitHub API call finishes
98
+ ```
99
+
100
+ ### `ErrorContext`
101
+
102
+ ```ts
103
+ interface ErrorContext {
104
+ tags?: Record<string, string> // Key-value pairs shown in the issue
105
+ extras?: Record<string, unknown> // JSON metadata in a collapsible section
106
+ user?: { id: string; email?: string }
107
+ requestUrl?: string
108
+ serverName?: string
109
+ }
110
+ ```
111
+
112
+ ## Framework guides
113
+
114
+ | Framework | Example | What it sets up |
115
+ |-----------|---------|----------------|
116
+ | **Next.js App Router** | [`examples/nextjs-instrumentation/`](examples/nextjs-instrumentation/) | Server-side `register()` + `onRequestError()` |
117
+ | **Next.js (client errors)** | [`examples/nextjs-error-proxy/`](examples/nextjs-error-proxy/) | Proxy endpoint for browser error boundaries |
118
+ | **Next.js (error UI)** | [`examples/nextjs-error-boundaries/`](examples/nextjs-error-boundaries/) | `error.tsx` and `global-error.tsx` components |
119
+ | **Express** | [`examples/express-middleware/`](examples/express-middleware/) | Error handler middleware |
120
+
121
+ ### Full Next.js setup (recommended)
122
+
123
+ For complete Next.js coverage, combine all three Next.js examples:
124
+
125
+ 1. **Server errors**: `instrumentation.ts` catches unhandled request errors
126
+ 2. **Client errors**: Error boundaries catch React errors and POST to the proxy
127
+ 3. **Proxy**: Server-side endpoint receives client errors and reports them (keeps token safe)
128
+
129
+ ## GitHub token setup
130
+
131
+ 1. Go to **GitHub → Settings → Developer settings → Fine-grained personal access tokens**
132
+ 2. Click **Generate new token**
133
+ 3. Set:
134
+ - **Repository access**: Only select repositories → choose your target repo
135
+ - **Permissions**: Issues → Read and write
136
+ 4. Copy the token and set it as `GITHUB_TOKEN` in your environment
137
+
138
+ > For classic tokens, the `repo` scope works but grants broader access than needed.
139
+
140
+ ## GitHub Issue structure
141
+
142
+ Issues created by the tracker look like this:
143
+
144
+ **Title**: `[Error] TypeError: Cannot read properties of undefined (reading 'map')`
145
+
146
+ **Labels**: `error-report`, `fingerprint:a1b2c3d4e5f6`, plus any custom labels
147
+
148
+ **Body**:
149
+ - Environment, fingerprint, and timestamp
150
+ - Error message
151
+ - Stack trace (code block)
152
+ - Tags, request URL, user info (if provided)
153
+ - Additional metadata (collapsible JSON)
154
+
155
+ ## Architecture
156
+
157
+ ### Fingerprinting
158
+
159
+ Errors are fingerprinted using SHA-256 of:
160
+ - Error name (e.g., `TypeError`)
161
+ - Message (first 100 characters)
162
+ - Top 3 normalized stack frames (line/column numbers, webpack hashes, and query strings stripped)
163
+
164
+ This produces a stable 12-character hex ID. The same logical error across different deploys produces the same fingerprint.
165
+
166
+ ### Deduplication
167
+
168
+ Two layers:
169
+ 1. **In-memory rate limiter**: Sliding window (max N new issues/min) + dedup window (suppress same fingerprint within 60s)
170
+ 2. **GitHub search**: Before creating an issue, search for existing issues by `fingerprint:<hash>` label
171
+
172
+ ### Rate limiting
173
+
174
+ - **Sliding window**: Max 10 new issues per minute (configurable)
175
+ - **Dedup window**: Same fingerprint suppressed for 60 seconds (configurable)
176
+ - Cleanup timer is `unref()`'d — never prevents Node.js process exit
177
+
178
+ ## Limitations
179
+
180
+ - **Node.js only**: Uses `node:crypto` for fingerprinting. Not compatible with browser or edge runtimes.
181
+ - **No session replay**: Unlike Sentry, there's no UI recording for debugging.
182
+ - **No performance tracing**: No APM, transaction monitoring, or request timing.
183
+ - **GitHub API rate limits**: 5,000 requests/hour for authenticated tokens. The in-memory rate limiter prevents hitting this in practice.
184
+ - **Dynamic error messages**: Errors with timestamps or IDs in the message may create separate issues. Keep the first 100 characters stable.
185
+
186
+ ## Requirements
187
+
188
+ - Node.js >= 18
189
+ - GitHub PAT with Issues read/write permission
190
+
191
+ ## License
192
+
193
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,363 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ captureException: () => captureException,
24
+ captureMessage: () => captureMessage,
25
+ flush: () => flush,
26
+ init: () => init
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+
30
+ // src/fingerprint.ts
31
+ var import_node_crypto = require("crypto");
32
+
33
+ // src/normalizer.ts
34
+ var LINE_COL_RE = /:\d+:\d+/g;
35
+ var WEBPACK_PREFIX_RE = /webpack-internal:\/\/\//g;
36
+ var QUERY_STRING_RE = /\?[^\s)]+/g;
37
+ var FRAME_RE = /^\s+at\s+/;
38
+ var NODE_MODULES_RE = /node_modules/;
39
+ function extractFrames(stack, maxFrames = 3) {
40
+ if (!stack) return [];
41
+ const lines = stack.split("\n");
42
+ const frames = [];
43
+ for (const line of lines) {
44
+ if (!FRAME_RE.test(line)) continue;
45
+ if (NODE_MODULES_RE.test(line)) continue;
46
+ const normalized = line.replace(WEBPACK_PREFIX_RE, "").replace(QUERY_STRING_RE, "").replace(LINE_COL_RE, "").trim();
47
+ frames.push(normalized);
48
+ if (frames.length >= maxFrames) break;
49
+ }
50
+ return frames;
51
+ }
52
+
53
+ // src/fingerprint.ts
54
+ var MESSAGE_TRUNCATE_LENGTH = 100;
55
+ function generateFingerprint(input) {
56
+ let name;
57
+ let message;
58
+ let stack;
59
+ if (typeof input === "string") {
60
+ name = "Error";
61
+ message = input;
62
+ stack = void 0;
63
+ } else {
64
+ name = input.name || "Error";
65
+ message = input.message || "";
66
+ stack = input.stack;
67
+ }
68
+ const truncatedMessage = message.slice(0, MESSAGE_TRUNCATE_LENGTH);
69
+ const frames = extractFrames(stack);
70
+ const payload = `${name}
71
+ ${truncatedMessage}
72
+ ${frames.join("\n")}`;
73
+ const hash = (0, import_node_crypto.createHash)("sha256").update(payload).digest("hex");
74
+ return hash.slice(0, 12);
75
+ }
76
+
77
+ // src/rate-limiter.ts
78
+ var ONE_MINUTE = 6e4;
79
+ var CLEANUP_INTERVAL = 5 * 6e4;
80
+ var RateLimiter = class {
81
+ config;
82
+ timestamps = [];
83
+ dedup = /* @__PURE__ */ new Map();
84
+ cleanupTimer = null;
85
+ constructor(config2) {
86
+ this.config = config2;
87
+ this.cleanupTimer = setInterval(() => this.cleanup(), CLEANUP_INTERVAL);
88
+ if (this.cleanupTimer && typeof this.cleanupTimer === "object" && "unref" in this.cleanupTimer) {
89
+ this.cleanupTimer.unref();
90
+ }
91
+ }
92
+ /**
93
+ * Check whether a fingerprint can be processed right now.
94
+ * Returns false if rate-limited or deduped.
95
+ */
96
+ canProcess(fingerprint) {
97
+ const now = Date.now();
98
+ const lastSeen = this.dedup.get(fingerprint);
99
+ if (lastSeen !== void 0 && now - lastSeen < this.config.dedupeWindowMs) {
100
+ return false;
101
+ }
102
+ this.pruneOldTimestamps(now);
103
+ return this.timestamps.length < this.config.maxPerMinute;
104
+ }
105
+ /**
106
+ * Record that a fingerprint was processed.
107
+ * Call this after successfully processing (or starting to process) an error.
108
+ */
109
+ recordProcessed(fingerprint) {
110
+ const now = Date.now();
111
+ this.timestamps.push(now);
112
+ this.dedup.set(fingerprint, now);
113
+ }
114
+ /** Stop the cleanup timer. Call on shutdown. */
115
+ destroy() {
116
+ if (this.cleanupTimer) {
117
+ clearInterval(this.cleanupTimer);
118
+ this.cleanupTimer = null;
119
+ }
120
+ }
121
+ pruneOldTimestamps(now) {
122
+ const cutoff = now - ONE_MINUTE;
123
+ while (this.timestamps.length > 0 && this.timestamps[0] < cutoff) {
124
+ this.timestamps.shift();
125
+ }
126
+ }
127
+ cleanup() {
128
+ const now = Date.now();
129
+ this.pruneOldTimestamps(now);
130
+ for (const [fp, ts] of this.dedup.entries()) {
131
+ if (now - ts >= this.config.dedupeWindowMs) {
132
+ this.dedup.delete(fp);
133
+ }
134
+ }
135
+ }
136
+ };
137
+
138
+ // src/github.ts
139
+ var import_octokit = require("octokit");
140
+ function parseRepo(repo) {
141
+ const [owner, name] = repo.split("/");
142
+ if (!owner || !name) {
143
+ throw new Error(`Invalid repo format "${repo}". Expected "owner/repo".`);
144
+ }
145
+ return { owner, repo: name };
146
+ }
147
+ function createGitHubClient(config2) {
148
+ const octokit = new import_octokit.Octokit({ auth: config2.token });
149
+ const { owner, repo } = parseRepo(config2.repo);
150
+ return {
151
+ async searchExistingIssue(fingerprint) {
152
+ try {
153
+ const { data } = await octokit.rest.issues.listForRepo({
154
+ owner,
155
+ repo,
156
+ labels: `fingerprint:${fingerprint}`,
157
+ state: "all",
158
+ per_page: 1
159
+ });
160
+ const issue = data[0];
161
+ if (!issue) return null;
162
+ return {
163
+ number: issue.number,
164
+ state: issue.state,
165
+ title: issue.title
166
+ };
167
+ } catch (err) {
168
+ config2.onError(err);
169
+ return null;
170
+ }
171
+ },
172
+ async createIssue(title, body, labels) {
173
+ try {
174
+ const { data } = await octokit.rest.issues.create({
175
+ owner,
176
+ repo,
177
+ title,
178
+ body,
179
+ labels
180
+ });
181
+ return data.number;
182
+ } catch (err) {
183
+ config2.onError(err);
184
+ return null;
185
+ }
186
+ },
187
+ async addReaction(issueNumber) {
188
+ try {
189
+ await octokit.rest.reactions.createForIssue({
190
+ owner,
191
+ repo,
192
+ issue_number: issueNumber,
193
+ content: "+1"
194
+ });
195
+ } catch (err) {
196
+ config2.onError(err);
197
+ }
198
+ },
199
+ async reopenIssue(issueNumber, comment) {
200
+ try {
201
+ await octokit.rest.issues.update({
202
+ owner,
203
+ repo,
204
+ issue_number: issueNumber,
205
+ state: "open"
206
+ });
207
+ await octokit.rest.issues.createComment({
208
+ owner,
209
+ repo,
210
+ issue_number: issueNumber,
211
+ body: comment
212
+ });
213
+ } catch (err) {
214
+ config2.onError(err);
215
+ }
216
+ }
217
+ };
218
+ }
219
+
220
+ // src/client.ts
221
+ var config = null;
222
+ var github = null;
223
+ var limiter = null;
224
+ var pending = [];
225
+ function init(cfg) {
226
+ config = {
227
+ environment: "development",
228
+ enabled: true,
229
+ reopenClosed: true,
230
+ rateLimitPerMinute: 10,
231
+ dedupeWindowMs: 6e4,
232
+ labels: [],
233
+ onError: console.error,
234
+ ...cfg
235
+ };
236
+ if (!config.enabled) return;
237
+ github = createGitHubClient({
238
+ token: config.githubToken,
239
+ repo: config.githubRepo,
240
+ onError: config.onError
241
+ });
242
+ limiter = new RateLimiter({
243
+ maxPerMinute: config.rateLimitPerMinute,
244
+ dedupeWindowMs: config.dedupeWindowMs
245
+ });
246
+ }
247
+ function captureException(error, context) {
248
+ if (!config?.enabled || !github || !limiter) return;
249
+ const fingerprint = generateFingerprint(error);
250
+ if (!limiter.canProcess(fingerprint)) {
251
+ console.error(`[error-tracker] Rate limited or deduped: ${fingerprint}`);
252
+ return;
253
+ }
254
+ limiter.recordProcessed(fingerprint);
255
+ const promise = processError(error, fingerprint, context).catch((err) => {
256
+ config?.onError?.(err);
257
+ });
258
+ pending.push(promise);
259
+ }
260
+ function captureMessage(message, level = "error", context) {
261
+ if (!config?.enabled || !github || !limiter) return;
262
+ const fingerprint = generateFingerprint(message);
263
+ if (!limiter.canProcess(fingerprint)) return;
264
+ limiter.recordProcessed(fingerprint);
265
+ const title = `[${level === "warning" ? "Warning" : "Error"}] ${message.slice(0, 80)}`;
266
+ const body = formatBody(message, void 0, fingerprint, context);
267
+ const labels = buildLabels(fingerprint);
268
+ const promise = github.searchExistingIssue(fingerprint).then(async (existing) => {
269
+ if (!github) return;
270
+ if (existing?.state === "open") {
271
+ await github.addReaction(existing.number);
272
+ } else if (existing?.state === "closed") {
273
+ if (config?.reopenClosed) {
274
+ await github.reopenIssue(existing.number, recurrenceComment());
275
+ await github.addReaction(existing.number);
276
+ }
277
+ } else {
278
+ await github.createIssue(title, body, labels);
279
+ }
280
+ }).catch((err) => {
281
+ config?.onError?.(err);
282
+ });
283
+ pending.push(promise);
284
+ }
285
+ async function flush() {
286
+ await Promise.allSettled(pending);
287
+ pending.length = 0;
288
+ }
289
+ async function processError(error, fingerprint, context) {
290
+ if (!github) return;
291
+ const existing = await github.searchExistingIssue(fingerprint);
292
+ if (existing?.state === "open") {
293
+ await github.addReaction(existing.number);
294
+ return;
295
+ }
296
+ if (existing?.state === "closed") {
297
+ if (config?.reopenClosed) {
298
+ await github.reopenIssue(existing.number, recurrenceComment());
299
+ await github.addReaction(existing.number);
300
+ }
301
+ return;
302
+ }
303
+ const title = `[Error] ${error.name || "Error"}: ${error.message.slice(0, 80)}`;
304
+ const body = formatBody(error.message, error.stack, fingerprint, context);
305
+ const labels = buildLabels(fingerprint);
306
+ await github.createIssue(title, body, labels);
307
+ }
308
+ function buildLabels(fingerprint) {
309
+ return [
310
+ "error-report",
311
+ `fingerprint:${fingerprint}`,
312
+ ...config?.labels ?? []
313
+ ];
314
+ }
315
+ function formatBody(message, stack, fingerprint, context) {
316
+ const env = config?.environment ?? "unknown";
317
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
318
+ const sections = [
319
+ `## Error Report (Automated)`,
320
+ `**Environment:** ${env} | **Fingerprint:** \`${fingerprint}\` | **Time:** ${timestamp}`,
321
+ "",
322
+ `### Message`,
323
+ message
324
+ ];
325
+ if (stack) {
326
+ sections.push("", "### Stack Trace", "```", stack, "```");
327
+ }
328
+ if (context?.tags && Object.keys(context.tags).length > 0) {
329
+ const tagLines = Object.entries(context.tags).map(([k, v]) => `- **${k}:** ${v}`).join("\n");
330
+ sections.push("", "### Tags", tagLines);
331
+ }
332
+ if (context?.requestUrl) {
333
+ sections.push("", `**Request URL:** ${context.requestUrl}`);
334
+ }
335
+ if (context?.user) {
336
+ sections.push(`**User:** ${context.user.id}${context.user.email ? ` (${context.user.email})` : ""}`);
337
+ }
338
+ if (context?.extras && Object.keys(context.extras).length > 0) {
339
+ sections.push(
340
+ "",
341
+ "<details>",
342
+ "<summary>Additional metadata</summary>",
343
+ "",
344
+ "```json",
345
+ JSON.stringify(context.extras, null, 2),
346
+ "```",
347
+ "</details>"
348
+ );
349
+ }
350
+ return sections.join("\n");
351
+ }
352
+ function recurrenceComment() {
353
+ const env = config?.environment ?? "unknown";
354
+ return `**Recurrence detected** at ${(/* @__PURE__ */ new Date()).toISOString()} in \`${env}\` environment.`;
355
+ }
356
+ // Annotate the CommonJS export names for ESM import in node:
357
+ 0 && (module.exports = {
358
+ captureException,
359
+ captureMessage,
360
+ flush,
361
+ init
362
+ });
363
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/fingerprint.ts","../src/normalizer.ts","../src/rate-limiter.ts","../src/github.ts","../src/client.ts"],"sourcesContent":["export { init, captureException, captureMessage, flush } from './client'\nexport type { ErrorTrackerConfig, ErrorContext } from './types'\n","/**\n * Error fingerprinting.\n *\n * Produces a deterministic hash from an error's name, message (truncated),\n * and top stack frames. Two occurrences of the \"same\" error — even across\n * different deploys with different line numbers — should produce the same\n * fingerprint.\n */\n\nimport { createHash } from 'node:crypto'\nimport { extractFrames } from './normalizer'\n\nconst MESSAGE_TRUNCATE_LENGTH = 100\n\n/**\n * Generate a 12-char hex fingerprint from an Error or plain string.\n */\nexport function generateFingerprint(input: Error | string): string {\n let name: string\n let message: string\n let stack: string | undefined\n\n if (typeof input === 'string') {\n name = 'Error'\n message = input\n stack = undefined\n } else {\n name = input.name || 'Error'\n message = input.message || ''\n stack = input.stack\n }\n\n const truncatedMessage = message.slice(0, MESSAGE_TRUNCATE_LENGTH)\n const frames = extractFrames(stack)\n const payload = `${name}\\n${truncatedMessage}\\n${frames.join('\\n')}`\n\n const hash = createHash('sha256').update(payload).digest('hex')\n return hash.slice(0, 12)\n}\n","/**\n * Stack trace normalizer.\n *\n * Strips volatile parts of stack traces (line/column numbers, webpack hashes,\n * query strings) so that the same logical error produces the same fingerprint\n * across deploys.\n */\n\n/** Strip line:col numbers like `:42:13` at the end of file paths */\nconst LINE_COL_RE = /:\\d+:\\d+/g\n\n/** Strip webpack-internal:/// prefix */\nconst WEBPACK_PREFIX_RE = /webpack-internal:\\/\\/\\//g\n\n/** Strip query strings from file paths like `?abc123` or `?v=hash` */\nconst QUERY_STRING_RE = /\\?[^\\s)]+/g\n\n/** Match stack frame lines (starts with \"at \" after whitespace) */\nconst FRAME_RE = /^\\s+at\\s+/\n\n/** Skip frames from node_modules */\nconst NODE_MODULES_RE = /node_modules/\n\n/**\n * Normalize a stack trace by removing volatile parts.\n * Returns a stable string suitable for hashing.\n */\nexport function normalizeStack(stack: string | undefined): string {\n if (!stack) return ''\n\n return stack\n .replace(WEBPACK_PREFIX_RE, '')\n .replace(QUERY_STRING_RE, '')\n .replace(LINE_COL_RE, '')\n}\n\n/**\n * Extract the first N meaningful (non-node_modules) frames from a stack trace.\n * Used for fingerprint generation — only top frames matter for identity.\n */\nexport function extractFrames(stack: string | undefined, maxFrames = 3): string[] {\n if (!stack) return []\n\n const lines = stack.split('\\n')\n const frames: string[] = []\n\n for (const line of lines) {\n if (!FRAME_RE.test(line)) continue\n if (NODE_MODULES_RE.test(line)) continue\n\n // Normalize the frame before collecting\n const normalized = line\n .replace(WEBPACK_PREFIX_RE, '')\n .replace(QUERY_STRING_RE, '')\n .replace(LINE_COL_RE, '')\n .trim()\n\n frames.push(normalized)\n if (frames.length >= maxFrames) break\n }\n\n return frames\n}\n","/**\n * Rate limiter with deduplication.\n *\n * Two layers of protection:\n * 1. Sliding window: max N issues created per minute\n * 2. Dedup window: suppress the same fingerprint within a configurable period\n */\n\nexport interface RateLimiterConfig {\n /** Max new issues per minute. */\n maxPerMinute: number\n /** Suppress duplicate fingerprints within this window (ms). */\n dedupeWindowMs: number\n}\n\nconst ONE_MINUTE = 60_000\nconst CLEANUP_INTERVAL = 5 * 60_000 // 5 minutes\n\nexport class RateLimiter {\n private readonly config: RateLimiterConfig\n private readonly timestamps: number[] = []\n private readonly dedup = new Map<string, number>()\n private cleanupTimer: ReturnType<typeof setInterval> | null = null\n\n constructor(config: RateLimiterConfig) {\n this.config = config\n this.cleanupTimer = setInterval(() => this.cleanup(), CLEANUP_INTERVAL)\n // Unref so the timer doesn't keep the process alive\n if (this.cleanupTimer && typeof this.cleanupTimer === 'object' && 'unref' in this.cleanupTimer) {\n this.cleanupTimer.unref()\n }\n }\n\n /**\n * Check whether a fingerprint can be processed right now.\n * Returns false if rate-limited or deduped.\n */\n canProcess(fingerprint: string): boolean {\n const now = Date.now()\n\n // Check dedup first\n const lastSeen = this.dedup.get(fingerprint)\n if (lastSeen !== undefined && now - lastSeen < this.config.dedupeWindowMs) {\n return false\n }\n\n // Check rate limit\n this.pruneOldTimestamps(now)\n return this.timestamps.length < this.config.maxPerMinute\n }\n\n /**\n * Record that a fingerprint was processed.\n * Call this after successfully processing (or starting to process) an error.\n */\n recordProcessed(fingerprint: string): void {\n const now = Date.now()\n this.timestamps.push(now)\n this.dedup.set(fingerprint, now)\n }\n\n /** Stop the cleanup timer. Call on shutdown. */\n destroy(): void {\n if (this.cleanupTimer) {\n clearInterval(this.cleanupTimer)\n this.cleanupTimer = null\n }\n }\n\n private pruneOldTimestamps(now: number): void {\n const cutoff = now - ONE_MINUTE\n while (this.timestamps.length > 0 && this.timestamps[0]! < cutoff) {\n this.timestamps.shift()\n }\n }\n\n private cleanup(): void {\n const now = Date.now()\n this.pruneOldTimestamps(now)\n\n for (const [fp, ts] of this.dedup.entries()) {\n if (now - ts >= this.config.dedupeWindowMs) {\n this.dedup.delete(fp)\n }\n }\n }\n}\n","/**\n * GitHub API layer for error tracking.\n *\n * All methods catch errors internally and delegate to `onError` —\n * they never throw. This ensures the error tracker itself never\n * crashes the host application.\n */\n\nimport { Octokit } from 'octokit'\nimport type { GitHubClient, ExistingIssue } from './types'\n\nexport interface GitHubClientConfig {\n token: string\n repo: string // \"owner/repo\"\n onError: (err: unknown) => void\n}\n\nfunction parseRepo(repo: string): { owner: string; repo: string } {\n const [owner, name] = repo.split('/')\n if (!owner || !name) {\n throw new Error(`Invalid repo format \"${repo}\". Expected \"owner/repo\".`)\n }\n return { owner, repo: name }\n}\n\nexport function createGitHubClient(config: GitHubClientConfig): GitHubClient {\n const octokit = new Octokit({ auth: config.token })\n const { owner, repo } = parseRepo(config.repo)\n\n return {\n async searchExistingIssue(fingerprint: string): Promise<ExistingIssue | null> {\n try {\n const { data } = await octokit.rest.issues.listForRepo({\n owner,\n repo,\n labels: `fingerprint:${fingerprint}`,\n state: 'all',\n per_page: 1,\n })\n\n const issue = data[0]\n if (!issue) return null\n\n return {\n number: issue.number,\n state: issue.state as 'open' | 'closed',\n title: issue.title,\n }\n } catch (err) {\n config.onError(err)\n return null\n }\n },\n\n async createIssue(title: string, body: string, labels: string[]): Promise<number | null> {\n try {\n const { data } = await octokit.rest.issues.create({\n owner,\n repo,\n title,\n body,\n labels,\n })\n return data.number\n } catch (err) {\n config.onError(err)\n return null\n }\n },\n\n async addReaction(issueNumber: number): Promise<void> {\n try {\n await octokit.rest.reactions.createForIssue({\n owner,\n repo,\n issue_number: issueNumber,\n content: '+1',\n })\n } catch (err) {\n config.onError(err)\n }\n },\n\n async reopenIssue(issueNumber: number, comment: string): Promise<void> {\n try {\n await octokit.rest.issues.update({\n owner,\n repo,\n issue_number: issueNumber,\n state: 'open',\n })\n await octokit.rest.issues.createComment({\n owner,\n repo,\n issue_number: issueNumber,\n body: comment,\n })\n } catch (err) {\n config.onError(err)\n }\n },\n }\n}\n","/**\n * Error tracker client — the main orchestrator.\n *\n * Singleton pattern: call `init()` once at startup, then use\n * `captureException()` / `captureMessage()` anywhere in your app.\n * All GitHub API calls are fire-and-forget with `flush()` for\n * serverless environments that need to wait before responding.\n */\n\nimport type { ErrorTrackerConfig, ErrorContext, GitHubClient } from './types'\nimport { generateFingerprint } from './fingerprint'\nimport { RateLimiter } from './rate-limiter'\nimport { createGitHubClient } from './github'\n\n// ---------------------------------------------------------------------------\n// Module state (singleton)\n// ---------------------------------------------------------------------------\n\nlet config: ErrorTrackerConfig | null = null\nlet github: GitHubClient | null = null\nlet limiter: RateLimiter | null = null\nconst pending: Promise<void>[] = []\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Initialize the error tracker. Call once at app startup.\n */\nexport function init(cfg: ErrorTrackerConfig): void {\n config = {\n environment: 'development',\n enabled: true,\n reopenClosed: true,\n rateLimitPerMinute: 10,\n dedupeWindowMs: 60_000,\n labels: [],\n onError: console.error,\n ...cfg,\n }\n\n if (!config.enabled) return\n\n github = createGitHubClient({\n token: config.githubToken,\n repo: config.githubRepo,\n onError: config.onError!,\n })\n\n limiter = new RateLimiter({\n maxPerMinute: config.rateLimitPerMinute!,\n dedupeWindowMs: config.dedupeWindowMs!,\n })\n}\n\n/**\n * Capture an exception. Fire-and-forget — use `flush()` if you\n * need to wait for the GitHub API call to complete.\n */\nexport function captureException(error: Error, context?: ErrorContext): void {\n if (!config?.enabled || !github || !limiter) return\n\n const fingerprint = generateFingerprint(error)\n\n if (!limiter.canProcess(fingerprint)) {\n console.error(`[error-tracker] Rate limited or deduped: ${fingerprint}`)\n return\n }\n\n limiter.recordProcessed(fingerprint)\n\n const promise = processError(error, fingerprint, context).catch((err) => {\n config?.onError?.(err)\n })\n\n pending.push(promise)\n}\n\n/**\n * Capture a plain message as an error event.\n */\nexport function captureMessage(\n message: string,\n level: 'error' | 'warning' = 'error',\n context?: ErrorContext,\n): void {\n if (!config?.enabled || !github || !limiter) return\n\n const fingerprint = generateFingerprint(message)\n\n if (!limiter.canProcess(fingerprint)) return\n\n limiter.recordProcessed(fingerprint)\n\n const title = `[${level === 'warning' ? 'Warning' : 'Error'}] ${message.slice(0, 80)}`\n const body = formatBody(message, undefined, fingerprint, context)\n const labels = buildLabels(fingerprint)\n\n const promise = github.searchExistingIssue(fingerprint).then(async (existing) => {\n if (!github) return\n\n if (existing?.state === 'open') {\n await github.addReaction(existing.number)\n } else if (existing?.state === 'closed') {\n if (config?.reopenClosed) {\n await github.reopenIssue(existing.number, recurrenceComment())\n await github.addReaction(existing.number)\n }\n } else {\n await github.createIssue(title, body, labels)\n }\n }).catch((err) => {\n config?.onError?.(err)\n })\n\n pending.push(promise)\n}\n\n/**\n * Wait for all pending error reports to complete.\n * Call before serverless function returns.\n */\nexport async function flush(): Promise<void> {\n await Promise.allSettled(pending)\n pending.length = 0\n}\n\n/**\n * Reset internal state. For testing only.\n * @internal\n */\nexport function _reset(): void {\n limiter?.destroy()\n config = null\n github = null\n limiter = null\n pending.length = 0\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nasync function processError(\n error: Error,\n fingerprint: string,\n context?: ErrorContext,\n): Promise<void> {\n if (!github) return\n\n const existing = await github.searchExistingIssue(fingerprint)\n\n if (existing?.state === 'open') {\n await github.addReaction(existing.number)\n return\n }\n\n if (existing?.state === 'closed') {\n if (config?.reopenClosed) {\n await github.reopenIssue(existing.number, recurrenceComment())\n await github.addReaction(existing.number)\n }\n return\n }\n\n // No existing issue — create one\n const title = `[Error] ${(error.name || 'Error')}: ${error.message.slice(0, 80)}`\n const body = formatBody(error.message, error.stack, fingerprint, context)\n const labels = buildLabels(fingerprint)\n\n await github.createIssue(title, body, labels)\n}\n\nfunction buildLabels(fingerprint: string): string[] {\n return [\n 'error-report',\n `fingerprint:${fingerprint}`,\n ...(config?.labels ?? []),\n ]\n}\n\nfunction formatBody(\n message: string,\n stack: string | undefined,\n fingerprint: string,\n context?: ErrorContext,\n): string {\n const env = config?.environment ?? 'unknown'\n const timestamp = new Date().toISOString()\n\n const sections = [\n `## Error Report (Automated)`,\n `**Environment:** ${env} | **Fingerprint:** \\`${fingerprint}\\` | **Time:** ${timestamp}`,\n '',\n `### Message`,\n message,\n ]\n\n if (stack) {\n sections.push('', '### Stack Trace', '```', stack, '```')\n }\n\n if (context?.tags && Object.keys(context.tags).length > 0) {\n const tagLines = Object.entries(context.tags)\n .map(([k, v]) => `- **${k}:** ${v}`)\n .join('\\n')\n sections.push('', '### Tags', tagLines)\n }\n\n if (context?.requestUrl) {\n sections.push('', `**Request URL:** ${context.requestUrl}`)\n }\n\n if (context?.user) {\n sections.push(`**User:** ${context.user.id}${context.user.email ? ` (${context.user.email})` : ''}`)\n }\n\n if (context?.extras && Object.keys(context.extras).length > 0) {\n sections.push(\n '',\n '<details>',\n '<summary>Additional metadata</summary>',\n '',\n '```json',\n JSON.stringify(context.extras, null, 2),\n '```',\n '</details>',\n )\n }\n\n return sections.join('\\n')\n}\n\nfunction recurrenceComment(): string {\n const env = config?.environment ?? 'unknown'\n return `**Recurrence detected** at ${new Date().toISOString()} in \\`${env}\\` environment.`\n}\n"],"mappings":"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","names":["config","config"]}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Configuration for the error tracker.
3
+ * Pass to `init()` at app startup.
4
+ */
5
+ interface ErrorTrackerConfig {
6
+ /** GitHub Personal Access Token with `repo` scope */
7
+ githubToken: string;
8
+ /** Repository in "owner/repo" format */
9
+ githubRepo: string;
10
+ /** Environment name included in issue body. Default: "development" */
11
+ environment?: string;
12
+ /** Additional labels applied to created issues (beyond "error-report"). */
13
+ labels?: string[];
14
+ /** Kill switch. Default: true */
15
+ enabled?: boolean;
16
+ /** Called when GitHub API fails. Default: console.error */
17
+ onError?: (err: unknown) => void;
18
+ /** Max new issues created per minute. Default: 10 */
19
+ rateLimitPerMinute?: number;
20
+ /** Suppress duplicate fingerprints within this window (ms). Default: 60_000 */
21
+ dedupeWindowMs?: number;
22
+ /** Reopen closed issues on recurrence instead of ignoring. Default: true */
23
+ reopenClosed?: boolean;
24
+ }
25
+ /**
26
+ * Additional context attached to a captured error.
27
+ */
28
+ interface ErrorContext {
29
+ tags?: Record<string, string>;
30
+ extras?: Record<string, unknown>;
31
+ user?: {
32
+ id: string;
33
+ email?: string;
34
+ };
35
+ requestUrl?: string;
36
+ serverName?: string;
37
+ }
38
+
39
+ /**
40
+ * Error tracker client — the main orchestrator.
41
+ *
42
+ * Singleton pattern: call `init()` once at startup, then use
43
+ * `captureException()` / `captureMessage()` anywhere in your app.
44
+ * All GitHub API calls are fire-and-forget with `flush()` for
45
+ * serverless environments that need to wait before responding.
46
+ */
47
+
48
+ /**
49
+ * Initialize the error tracker. Call once at app startup.
50
+ */
51
+ declare function init(cfg: ErrorTrackerConfig): void;
52
+ /**
53
+ * Capture an exception. Fire-and-forget — use `flush()` if you
54
+ * need to wait for the GitHub API call to complete.
55
+ */
56
+ declare function captureException(error: Error, context?: ErrorContext): void;
57
+ /**
58
+ * Capture a plain message as an error event.
59
+ */
60
+ declare function captureMessage(message: string, level?: 'error' | 'warning', context?: ErrorContext): void;
61
+ /**
62
+ * Wait for all pending error reports to complete.
63
+ * Call before serverless function returns.
64
+ */
65
+ declare function flush(): Promise<void>;
66
+
67
+ export { type ErrorContext, type ErrorTrackerConfig, captureException, captureMessage, flush, init };
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Configuration for the error tracker.
3
+ * Pass to `init()` at app startup.
4
+ */
5
+ interface ErrorTrackerConfig {
6
+ /** GitHub Personal Access Token with `repo` scope */
7
+ githubToken: string;
8
+ /** Repository in "owner/repo" format */
9
+ githubRepo: string;
10
+ /** Environment name included in issue body. Default: "development" */
11
+ environment?: string;
12
+ /** Additional labels applied to created issues (beyond "error-report"). */
13
+ labels?: string[];
14
+ /** Kill switch. Default: true */
15
+ enabled?: boolean;
16
+ /** Called when GitHub API fails. Default: console.error */
17
+ onError?: (err: unknown) => void;
18
+ /** Max new issues created per minute. Default: 10 */
19
+ rateLimitPerMinute?: number;
20
+ /** Suppress duplicate fingerprints within this window (ms). Default: 60_000 */
21
+ dedupeWindowMs?: number;
22
+ /** Reopen closed issues on recurrence instead of ignoring. Default: true */
23
+ reopenClosed?: boolean;
24
+ }
25
+ /**
26
+ * Additional context attached to a captured error.
27
+ */
28
+ interface ErrorContext {
29
+ tags?: Record<string, string>;
30
+ extras?: Record<string, unknown>;
31
+ user?: {
32
+ id: string;
33
+ email?: string;
34
+ };
35
+ requestUrl?: string;
36
+ serverName?: string;
37
+ }
38
+
39
+ /**
40
+ * Error tracker client — the main orchestrator.
41
+ *
42
+ * Singleton pattern: call `init()` once at startup, then use
43
+ * `captureException()` / `captureMessage()` anywhere in your app.
44
+ * All GitHub API calls are fire-and-forget with `flush()` for
45
+ * serverless environments that need to wait before responding.
46
+ */
47
+
48
+ /**
49
+ * Initialize the error tracker. Call once at app startup.
50
+ */
51
+ declare function init(cfg: ErrorTrackerConfig): void;
52
+ /**
53
+ * Capture an exception. Fire-and-forget — use `flush()` if you
54
+ * need to wait for the GitHub API call to complete.
55
+ */
56
+ declare function captureException(error: Error, context?: ErrorContext): void;
57
+ /**
58
+ * Capture a plain message as an error event.
59
+ */
60
+ declare function captureMessage(message: string, level?: 'error' | 'warning', context?: ErrorContext): void;
61
+ /**
62
+ * Wait for all pending error reports to complete.
63
+ * Call before serverless function returns.
64
+ */
65
+ declare function flush(): Promise<void>;
66
+
67
+ export { type ErrorContext, type ErrorTrackerConfig, captureException, captureMessage, flush, init };
package/dist/index.js ADDED
@@ -0,0 +1,333 @@
1
+ // src/fingerprint.ts
2
+ import { createHash } from "crypto";
3
+
4
+ // src/normalizer.ts
5
+ var LINE_COL_RE = /:\d+:\d+/g;
6
+ var WEBPACK_PREFIX_RE = /webpack-internal:\/\/\//g;
7
+ var QUERY_STRING_RE = /\?[^\s)]+/g;
8
+ var FRAME_RE = /^\s+at\s+/;
9
+ var NODE_MODULES_RE = /node_modules/;
10
+ function extractFrames(stack, maxFrames = 3) {
11
+ if (!stack) return [];
12
+ const lines = stack.split("\n");
13
+ const frames = [];
14
+ for (const line of lines) {
15
+ if (!FRAME_RE.test(line)) continue;
16
+ if (NODE_MODULES_RE.test(line)) continue;
17
+ const normalized = line.replace(WEBPACK_PREFIX_RE, "").replace(QUERY_STRING_RE, "").replace(LINE_COL_RE, "").trim();
18
+ frames.push(normalized);
19
+ if (frames.length >= maxFrames) break;
20
+ }
21
+ return frames;
22
+ }
23
+
24
+ // src/fingerprint.ts
25
+ var MESSAGE_TRUNCATE_LENGTH = 100;
26
+ function generateFingerprint(input) {
27
+ let name;
28
+ let message;
29
+ let stack;
30
+ if (typeof input === "string") {
31
+ name = "Error";
32
+ message = input;
33
+ stack = void 0;
34
+ } else {
35
+ name = input.name || "Error";
36
+ message = input.message || "";
37
+ stack = input.stack;
38
+ }
39
+ const truncatedMessage = message.slice(0, MESSAGE_TRUNCATE_LENGTH);
40
+ const frames = extractFrames(stack);
41
+ const payload = `${name}
42
+ ${truncatedMessage}
43
+ ${frames.join("\n")}`;
44
+ const hash = createHash("sha256").update(payload).digest("hex");
45
+ return hash.slice(0, 12);
46
+ }
47
+
48
+ // src/rate-limiter.ts
49
+ var ONE_MINUTE = 6e4;
50
+ var CLEANUP_INTERVAL = 5 * 6e4;
51
+ var RateLimiter = class {
52
+ config;
53
+ timestamps = [];
54
+ dedup = /* @__PURE__ */ new Map();
55
+ cleanupTimer = null;
56
+ constructor(config2) {
57
+ this.config = config2;
58
+ this.cleanupTimer = setInterval(() => this.cleanup(), CLEANUP_INTERVAL);
59
+ if (this.cleanupTimer && typeof this.cleanupTimer === "object" && "unref" in this.cleanupTimer) {
60
+ this.cleanupTimer.unref();
61
+ }
62
+ }
63
+ /**
64
+ * Check whether a fingerprint can be processed right now.
65
+ * Returns false if rate-limited or deduped.
66
+ */
67
+ canProcess(fingerprint) {
68
+ const now = Date.now();
69
+ const lastSeen = this.dedup.get(fingerprint);
70
+ if (lastSeen !== void 0 && now - lastSeen < this.config.dedupeWindowMs) {
71
+ return false;
72
+ }
73
+ this.pruneOldTimestamps(now);
74
+ return this.timestamps.length < this.config.maxPerMinute;
75
+ }
76
+ /**
77
+ * Record that a fingerprint was processed.
78
+ * Call this after successfully processing (or starting to process) an error.
79
+ */
80
+ recordProcessed(fingerprint) {
81
+ const now = Date.now();
82
+ this.timestamps.push(now);
83
+ this.dedup.set(fingerprint, now);
84
+ }
85
+ /** Stop the cleanup timer. Call on shutdown. */
86
+ destroy() {
87
+ if (this.cleanupTimer) {
88
+ clearInterval(this.cleanupTimer);
89
+ this.cleanupTimer = null;
90
+ }
91
+ }
92
+ pruneOldTimestamps(now) {
93
+ const cutoff = now - ONE_MINUTE;
94
+ while (this.timestamps.length > 0 && this.timestamps[0] < cutoff) {
95
+ this.timestamps.shift();
96
+ }
97
+ }
98
+ cleanup() {
99
+ const now = Date.now();
100
+ this.pruneOldTimestamps(now);
101
+ for (const [fp, ts] of this.dedup.entries()) {
102
+ if (now - ts >= this.config.dedupeWindowMs) {
103
+ this.dedup.delete(fp);
104
+ }
105
+ }
106
+ }
107
+ };
108
+
109
+ // src/github.ts
110
+ import { Octokit } from "octokit";
111
+ function parseRepo(repo) {
112
+ const [owner, name] = repo.split("/");
113
+ if (!owner || !name) {
114
+ throw new Error(`Invalid repo format "${repo}". Expected "owner/repo".`);
115
+ }
116
+ return { owner, repo: name };
117
+ }
118
+ function createGitHubClient(config2) {
119
+ const octokit = new Octokit({ auth: config2.token });
120
+ const { owner, repo } = parseRepo(config2.repo);
121
+ return {
122
+ async searchExistingIssue(fingerprint) {
123
+ try {
124
+ const { data } = await octokit.rest.issues.listForRepo({
125
+ owner,
126
+ repo,
127
+ labels: `fingerprint:${fingerprint}`,
128
+ state: "all",
129
+ per_page: 1
130
+ });
131
+ const issue = data[0];
132
+ if (!issue) return null;
133
+ return {
134
+ number: issue.number,
135
+ state: issue.state,
136
+ title: issue.title
137
+ };
138
+ } catch (err) {
139
+ config2.onError(err);
140
+ return null;
141
+ }
142
+ },
143
+ async createIssue(title, body, labels) {
144
+ try {
145
+ const { data } = await octokit.rest.issues.create({
146
+ owner,
147
+ repo,
148
+ title,
149
+ body,
150
+ labels
151
+ });
152
+ return data.number;
153
+ } catch (err) {
154
+ config2.onError(err);
155
+ return null;
156
+ }
157
+ },
158
+ async addReaction(issueNumber) {
159
+ try {
160
+ await octokit.rest.reactions.createForIssue({
161
+ owner,
162
+ repo,
163
+ issue_number: issueNumber,
164
+ content: "+1"
165
+ });
166
+ } catch (err) {
167
+ config2.onError(err);
168
+ }
169
+ },
170
+ async reopenIssue(issueNumber, comment) {
171
+ try {
172
+ await octokit.rest.issues.update({
173
+ owner,
174
+ repo,
175
+ issue_number: issueNumber,
176
+ state: "open"
177
+ });
178
+ await octokit.rest.issues.createComment({
179
+ owner,
180
+ repo,
181
+ issue_number: issueNumber,
182
+ body: comment
183
+ });
184
+ } catch (err) {
185
+ config2.onError(err);
186
+ }
187
+ }
188
+ };
189
+ }
190
+
191
+ // src/client.ts
192
+ var config = null;
193
+ var github = null;
194
+ var limiter = null;
195
+ var pending = [];
196
+ function init(cfg) {
197
+ config = {
198
+ environment: "development",
199
+ enabled: true,
200
+ reopenClosed: true,
201
+ rateLimitPerMinute: 10,
202
+ dedupeWindowMs: 6e4,
203
+ labels: [],
204
+ onError: console.error,
205
+ ...cfg
206
+ };
207
+ if (!config.enabled) return;
208
+ github = createGitHubClient({
209
+ token: config.githubToken,
210
+ repo: config.githubRepo,
211
+ onError: config.onError
212
+ });
213
+ limiter = new RateLimiter({
214
+ maxPerMinute: config.rateLimitPerMinute,
215
+ dedupeWindowMs: config.dedupeWindowMs
216
+ });
217
+ }
218
+ function captureException(error, context) {
219
+ if (!config?.enabled || !github || !limiter) return;
220
+ const fingerprint = generateFingerprint(error);
221
+ if (!limiter.canProcess(fingerprint)) {
222
+ console.error(`[error-tracker] Rate limited or deduped: ${fingerprint}`);
223
+ return;
224
+ }
225
+ limiter.recordProcessed(fingerprint);
226
+ const promise = processError(error, fingerprint, context).catch((err) => {
227
+ config?.onError?.(err);
228
+ });
229
+ pending.push(promise);
230
+ }
231
+ function captureMessage(message, level = "error", context) {
232
+ if (!config?.enabled || !github || !limiter) return;
233
+ const fingerprint = generateFingerprint(message);
234
+ if (!limiter.canProcess(fingerprint)) return;
235
+ limiter.recordProcessed(fingerprint);
236
+ const title = `[${level === "warning" ? "Warning" : "Error"}] ${message.slice(0, 80)}`;
237
+ const body = formatBody(message, void 0, fingerprint, context);
238
+ const labels = buildLabels(fingerprint);
239
+ const promise = github.searchExistingIssue(fingerprint).then(async (existing) => {
240
+ if (!github) return;
241
+ if (existing?.state === "open") {
242
+ await github.addReaction(existing.number);
243
+ } else if (existing?.state === "closed") {
244
+ if (config?.reopenClosed) {
245
+ await github.reopenIssue(existing.number, recurrenceComment());
246
+ await github.addReaction(existing.number);
247
+ }
248
+ } else {
249
+ await github.createIssue(title, body, labels);
250
+ }
251
+ }).catch((err) => {
252
+ config?.onError?.(err);
253
+ });
254
+ pending.push(promise);
255
+ }
256
+ async function flush() {
257
+ await Promise.allSettled(pending);
258
+ pending.length = 0;
259
+ }
260
+ async function processError(error, fingerprint, context) {
261
+ if (!github) return;
262
+ const existing = await github.searchExistingIssue(fingerprint);
263
+ if (existing?.state === "open") {
264
+ await github.addReaction(existing.number);
265
+ return;
266
+ }
267
+ if (existing?.state === "closed") {
268
+ if (config?.reopenClosed) {
269
+ await github.reopenIssue(existing.number, recurrenceComment());
270
+ await github.addReaction(existing.number);
271
+ }
272
+ return;
273
+ }
274
+ const title = `[Error] ${error.name || "Error"}: ${error.message.slice(0, 80)}`;
275
+ const body = formatBody(error.message, error.stack, fingerprint, context);
276
+ const labels = buildLabels(fingerprint);
277
+ await github.createIssue(title, body, labels);
278
+ }
279
+ function buildLabels(fingerprint) {
280
+ return [
281
+ "error-report",
282
+ `fingerprint:${fingerprint}`,
283
+ ...config?.labels ?? []
284
+ ];
285
+ }
286
+ function formatBody(message, stack, fingerprint, context) {
287
+ const env = config?.environment ?? "unknown";
288
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
289
+ const sections = [
290
+ `## Error Report (Automated)`,
291
+ `**Environment:** ${env} | **Fingerprint:** \`${fingerprint}\` | **Time:** ${timestamp}`,
292
+ "",
293
+ `### Message`,
294
+ message
295
+ ];
296
+ if (stack) {
297
+ sections.push("", "### Stack Trace", "```", stack, "```");
298
+ }
299
+ if (context?.tags && Object.keys(context.tags).length > 0) {
300
+ const tagLines = Object.entries(context.tags).map(([k, v]) => `- **${k}:** ${v}`).join("\n");
301
+ sections.push("", "### Tags", tagLines);
302
+ }
303
+ if (context?.requestUrl) {
304
+ sections.push("", `**Request URL:** ${context.requestUrl}`);
305
+ }
306
+ if (context?.user) {
307
+ sections.push(`**User:** ${context.user.id}${context.user.email ? ` (${context.user.email})` : ""}`);
308
+ }
309
+ if (context?.extras && Object.keys(context.extras).length > 0) {
310
+ sections.push(
311
+ "",
312
+ "<details>",
313
+ "<summary>Additional metadata</summary>",
314
+ "",
315
+ "```json",
316
+ JSON.stringify(context.extras, null, 2),
317
+ "```",
318
+ "</details>"
319
+ );
320
+ }
321
+ return sections.join("\n");
322
+ }
323
+ function recurrenceComment() {
324
+ const env = config?.environment ?? "unknown";
325
+ return `**Recurrence detected** at ${(/* @__PURE__ */ new Date()).toISOString()} in \`${env}\` environment.`;
326
+ }
327
+ export {
328
+ captureException,
329
+ captureMessage,
330
+ flush,
331
+ init
332
+ };
333
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/fingerprint.ts","../src/normalizer.ts","../src/rate-limiter.ts","../src/github.ts","../src/client.ts"],"sourcesContent":["/**\n * Error fingerprinting.\n *\n * Produces a deterministic hash from an error's name, message (truncated),\n * and top stack frames. Two occurrences of the \"same\" error — even across\n * different deploys with different line numbers — should produce the same\n * fingerprint.\n */\n\nimport { createHash } from 'node:crypto'\nimport { extractFrames } from './normalizer'\n\nconst MESSAGE_TRUNCATE_LENGTH = 100\n\n/**\n * Generate a 12-char hex fingerprint from an Error or plain string.\n */\nexport function generateFingerprint(input: Error | string): string {\n let name: string\n let message: string\n let stack: string | undefined\n\n if (typeof input === 'string') {\n name = 'Error'\n message = input\n stack = undefined\n } else {\n name = input.name || 'Error'\n message = input.message || ''\n stack = input.stack\n }\n\n const truncatedMessage = message.slice(0, MESSAGE_TRUNCATE_LENGTH)\n const frames = extractFrames(stack)\n const payload = `${name}\\n${truncatedMessage}\\n${frames.join('\\n')}`\n\n const hash = createHash('sha256').update(payload).digest('hex')\n return hash.slice(0, 12)\n}\n","/**\n * Stack trace normalizer.\n *\n * Strips volatile parts of stack traces (line/column numbers, webpack hashes,\n * query strings) so that the same logical error produces the same fingerprint\n * across deploys.\n */\n\n/** Strip line:col numbers like `:42:13` at the end of file paths */\nconst LINE_COL_RE = /:\\d+:\\d+/g\n\n/** Strip webpack-internal:/// prefix */\nconst WEBPACK_PREFIX_RE = /webpack-internal:\\/\\/\\//g\n\n/** Strip query strings from file paths like `?abc123` or `?v=hash` */\nconst QUERY_STRING_RE = /\\?[^\\s)]+/g\n\n/** Match stack frame lines (starts with \"at \" after whitespace) */\nconst FRAME_RE = /^\\s+at\\s+/\n\n/** Skip frames from node_modules */\nconst NODE_MODULES_RE = /node_modules/\n\n/**\n * Normalize a stack trace by removing volatile parts.\n * Returns a stable string suitable for hashing.\n */\nexport function normalizeStack(stack: string | undefined): string {\n if (!stack) return ''\n\n return stack\n .replace(WEBPACK_PREFIX_RE, '')\n .replace(QUERY_STRING_RE, '')\n .replace(LINE_COL_RE, '')\n}\n\n/**\n * Extract the first N meaningful (non-node_modules) frames from a stack trace.\n * Used for fingerprint generation — only top frames matter for identity.\n */\nexport function extractFrames(stack: string | undefined, maxFrames = 3): string[] {\n if (!stack) return []\n\n const lines = stack.split('\\n')\n const frames: string[] = []\n\n for (const line of lines) {\n if (!FRAME_RE.test(line)) continue\n if (NODE_MODULES_RE.test(line)) continue\n\n // Normalize the frame before collecting\n const normalized = line\n .replace(WEBPACK_PREFIX_RE, '')\n .replace(QUERY_STRING_RE, '')\n .replace(LINE_COL_RE, '')\n .trim()\n\n frames.push(normalized)\n if (frames.length >= maxFrames) break\n }\n\n return frames\n}\n","/**\n * Rate limiter with deduplication.\n *\n * Two layers of protection:\n * 1. Sliding window: max N issues created per minute\n * 2. Dedup window: suppress the same fingerprint within a configurable period\n */\n\nexport interface RateLimiterConfig {\n /** Max new issues per minute. */\n maxPerMinute: number\n /** Suppress duplicate fingerprints within this window (ms). */\n dedupeWindowMs: number\n}\n\nconst ONE_MINUTE = 60_000\nconst CLEANUP_INTERVAL = 5 * 60_000 // 5 minutes\n\nexport class RateLimiter {\n private readonly config: RateLimiterConfig\n private readonly timestamps: number[] = []\n private readonly dedup = new Map<string, number>()\n private cleanupTimer: ReturnType<typeof setInterval> | null = null\n\n constructor(config: RateLimiterConfig) {\n this.config = config\n this.cleanupTimer = setInterval(() => this.cleanup(), CLEANUP_INTERVAL)\n // Unref so the timer doesn't keep the process alive\n if (this.cleanupTimer && typeof this.cleanupTimer === 'object' && 'unref' in this.cleanupTimer) {\n this.cleanupTimer.unref()\n }\n }\n\n /**\n * Check whether a fingerprint can be processed right now.\n * Returns false if rate-limited or deduped.\n */\n canProcess(fingerprint: string): boolean {\n const now = Date.now()\n\n // Check dedup first\n const lastSeen = this.dedup.get(fingerprint)\n if (lastSeen !== undefined && now - lastSeen < this.config.dedupeWindowMs) {\n return false\n }\n\n // Check rate limit\n this.pruneOldTimestamps(now)\n return this.timestamps.length < this.config.maxPerMinute\n }\n\n /**\n * Record that a fingerprint was processed.\n * Call this after successfully processing (or starting to process) an error.\n */\n recordProcessed(fingerprint: string): void {\n const now = Date.now()\n this.timestamps.push(now)\n this.dedup.set(fingerprint, now)\n }\n\n /** Stop the cleanup timer. Call on shutdown. */\n destroy(): void {\n if (this.cleanupTimer) {\n clearInterval(this.cleanupTimer)\n this.cleanupTimer = null\n }\n }\n\n private pruneOldTimestamps(now: number): void {\n const cutoff = now - ONE_MINUTE\n while (this.timestamps.length > 0 && this.timestamps[0]! < cutoff) {\n this.timestamps.shift()\n }\n }\n\n private cleanup(): void {\n const now = Date.now()\n this.pruneOldTimestamps(now)\n\n for (const [fp, ts] of this.dedup.entries()) {\n if (now - ts >= this.config.dedupeWindowMs) {\n this.dedup.delete(fp)\n }\n }\n }\n}\n","/**\n * GitHub API layer for error tracking.\n *\n * All methods catch errors internally and delegate to `onError` —\n * they never throw. This ensures the error tracker itself never\n * crashes the host application.\n */\n\nimport { Octokit } from 'octokit'\nimport type { GitHubClient, ExistingIssue } from './types'\n\nexport interface GitHubClientConfig {\n token: string\n repo: string // \"owner/repo\"\n onError: (err: unknown) => void\n}\n\nfunction parseRepo(repo: string): { owner: string; repo: string } {\n const [owner, name] = repo.split('/')\n if (!owner || !name) {\n throw new Error(`Invalid repo format \"${repo}\". Expected \"owner/repo\".`)\n }\n return { owner, repo: name }\n}\n\nexport function createGitHubClient(config: GitHubClientConfig): GitHubClient {\n const octokit = new Octokit({ auth: config.token })\n const { owner, repo } = parseRepo(config.repo)\n\n return {\n async searchExistingIssue(fingerprint: string): Promise<ExistingIssue | null> {\n try {\n const { data } = await octokit.rest.issues.listForRepo({\n owner,\n repo,\n labels: `fingerprint:${fingerprint}`,\n state: 'all',\n per_page: 1,\n })\n\n const issue = data[0]\n if (!issue) return null\n\n return {\n number: issue.number,\n state: issue.state as 'open' | 'closed',\n title: issue.title,\n }\n } catch (err) {\n config.onError(err)\n return null\n }\n },\n\n async createIssue(title: string, body: string, labels: string[]): Promise<number | null> {\n try {\n const { data } = await octokit.rest.issues.create({\n owner,\n repo,\n title,\n body,\n labels,\n })\n return data.number\n } catch (err) {\n config.onError(err)\n return null\n }\n },\n\n async addReaction(issueNumber: number): Promise<void> {\n try {\n await octokit.rest.reactions.createForIssue({\n owner,\n repo,\n issue_number: issueNumber,\n content: '+1',\n })\n } catch (err) {\n config.onError(err)\n }\n },\n\n async reopenIssue(issueNumber: number, comment: string): Promise<void> {\n try {\n await octokit.rest.issues.update({\n owner,\n repo,\n issue_number: issueNumber,\n state: 'open',\n })\n await octokit.rest.issues.createComment({\n owner,\n repo,\n issue_number: issueNumber,\n body: comment,\n })\n } catch (err) {\n config.onError(err)\n }\n },\n }\n}\n","/**\n * Error tracker client — the main orchestrator.\n *\n * Singleton pattern: call `init()` once at startup, then use\n * `captureException()` / `captureMessage()` anywhere in your app.\n * All GitHub API calls are fire-and-forget with `flush()` for\n * serverless environments that need to wait before responding.\n */\n\nimport type { ErrorTrackerConfig, ErrorContext, GitHubClient } from './types'\nimport { generateFingerprint } from './fingerprint'\nimport { RateLimiter } from './rate-limiter'\nimport { createGitHubClient } from './github'\n\n// ---------------------------------------------------------------------------\n// Module state (singleton)\n// ---------------------------------------------------------------------------\n\nlet config: ErrorTrackerConfig | null = null\nlet github: GitHubClient | null = null\nlet limiter: RateLimiter | null = null\nconst pending: Promise<void>[] = []\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Initialize the error tracker. Call once at app startup.\n */\nexport function init(cfg: ErrorTrackerConfig): void {\n config = {\n environment: 'development',\n enabled: true,\n reopenClosed: true,\n rateLimitPerMinute: 10,\n dedupeWindowMs: 60_000,\n labels: [],\n onError: console.error,\n ...cfg,\n }\n\n if (!config.enabled) return\n\n github = createGitHubClient({\n token: config.githubToken,\n repo: config.githubRepo,\n onError: config.onError!,\n })\n\n limiter = new RateLimiter({\n maxPerMinute: config.rateLimitPerMinute!,\n dedupeWindowMs: config.dedupeWindowMs!,\n })\n}\n\n/**\n * Capture an exception. Fire-and-forget — use `flush()` if you\n * need to wait for the GitHub API call to complete.\n */\nexport function captureException(error: Error, context?: ErrorContext): void {\n if (!config?.enabled || !github || !limiter) return\n\n const fingerprint = generateFingerprint(error)\n\n if (!limiter.canProcess(fingerprint)) {\n console.error(`[error-tracker] Rate limited or deduped: ${fingerprint}`)\n return\n }\n\n limiter.recordProcessed(fingerprint)\n\n const promise = processError(error, fingerprint, context).catch((err) => {\n config?.onError?.(err)\n })\n\n pending.push(promise)\n}\n\n/**\n * Capture a plain message as an error event.\n */\nexport function captureMessage(\n message: string,\n level: 'error' | 'warning' = 'error',\n context?: ErrorContext,\n): void {\n if (!config?.enabled || !github || !limiter) return\n\n const fingerprint = generateFingerprint(message)\n\n if (!limiter.canProcess(fingerprint)) return\n\n limiter.recordProcessed(fingerprint)\n\n const title = `[${level === 'warning' ? 'Warning' : 'Error'}] ${message.slice(0, 80)}`\n const body = formatBody(message, undefined, fingerprint, context)\n const labels = buildLabels(fingerprint)\n\n const promise = github.searchExistingIssue(fingerprint).then(async (existing) => {\n if (!github) return\n\n if (existing?.state === 'open') {\n await github.addReaction(existing.number)\n } else if (existing?.state === 'closed') {\n if (config?.reopenClosed) {\n await github.reopenIssue(existing.number, recurrenceComment())\n await github.addReaction(existing.number)\n }\n } else {\n await github.createIssue(title, body, labels)\n }\n }).catch((err) => {\n config?.onError?.(err)\n })\n\n pending.push(promise)\n}\n\n/**\n * Wait for all pending error reports to complete.\n * Call before serverless function returns.\n */\nexport async function flush(): Promise<void> {\n await Promise.allSettled(pending)\n pending.length = 0\n}\n\n/**\n * Reset internal state. For testing only.\n * @internal\n */\nexport function _reset(): void {\n limiter?.destroy()\n config = null\n github = null\n limiter = null\n pending.length = 0\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nasync function processError(\n error: Error,\n fingerprint: string,\n context?: ErrorContext,\n): Promise<void> {\n if (!github) return\n\n const existing = await github.searchExistingIssue(fingerprint)\n\n if (existing?.state === 'open') {\n await github.addReaction(existing.number)\n return\n }\n\n if (existing?.state === 'closed') {\n if (config?.reopenClosed) {\n await github.reopenIssue(existing.number, recurrenceComment())\n await github.addReaction(existing.number)\n }\n return\n }\n\n // No existing issue — create one\n const title = `[Error] ${(error.name || 'Error')}: ${error.message.slice(0, 80)}`\n const body = formatBody(error.message, error.stack, fingerprint, context)\n const labels = buildLabels(fingerprint)\n\n await github.createIssue(title, body, labels)\n}\n\nfunction buildLabels(fingerprint: string): string[] {\n return [\n 'error-report',\n `fingerprint:${fingerprint}`,\n ...(config?.labels ?? []),\n ]\n}\n\nfunction formatBody(\n message: string,\n stack: string | undefined,\n fingerprint: string,\n context?: ErrorContext,\n): string {\n const env = config?.environment ?? 'unknown'\n const timestamp = new Date().toISOString()\n\n const sections = [\n `## Error Report (Automated)`,\n `**Environment:** ${env} | **Fingerprint:** \\`${fingerprint}\\` | **Time:** ${timestamp}`,\n '',\n `### Message`,\n message,\n ]\n\n if (stack) {\n sections.push('', '### Stack Trace', '```', stack, '```')\n }\n\n if (context?.tags && Object.keys(context.tags).length > 0) {\n const tagLines = Object.entries(context.tags)\n .map(([k, v]) => `- **${k}:** ${v}`)\n .join('\\n')\n sections.push('', '### Tags', tagLines)\n }\n\n if (context?.requestUrl) {\n sections.push('', `**Request URL:** ${context.requestUrl}`)\n }\n\n if (context?.user) {\n sections.push(`**User:** ${context.user.id}${context.user.email ? ` (${context.user.email})` : ''}`)\n }\n\n if (context?.extras && Object.keys(context.extras).length > 0) {\n sections.push(\n '',\n '<details>',\n '<summary>Additional metadata</summary>',\n '',\n '```json',\n JSON.stringify(context.extras, null, 2),\n '```',\n '</details>',\n )\n }\n\n return sections.join('\\n')\n}\n\nfunction recurrenceComment(): string {\n const env = config?.environment ?? 'unknown'\n return `**Recurrence detected** at ${new Date().toISOString()} in \\`${env}\\` environment.`\n}\n"],"mappings":"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","names":["config","config"]}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "gh-issue-tracker",
3
+ "version": "1.0.0",
4
+ "description": "Lightweight error tracking that creates GitHub Issues instead of sending to SaaS. Deduplication, fingerprinting, and rate limiting built-in.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsup",
23
+ "test": "vitest run",
24
+ "test:watch": "vitest",
25
+ "type-check": "tsc --noEmit",
26
+ "prepublishOnly": "pnpm build"
27
+ },
28
+ "keywords": [
29
+ "error-tracking",
30
+ "github-issues",
31
+ "error-reporter",
32
+ "sentry-alternative",
33
+ "fingerprinting",
34
+ "deduplication"
35
+ ],
36
+ "license": "MIT",
37
+ "engines": {
38
+ "node": ">=18"
39
+ },
40
+ "dependencies": {
41
+ "octokit": "^4.1.2"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^22.9.0",
45
+ "tsup": "^8.0.0",
46
+ "typescript": "^5.7.2",
47
+ "vitest": "^4.0.16"
48
+ }
49
+ }