Skip to content

Commit 0ea8130

Browse files
committed
fix: add crash report prompt
1 parent f0294b4 commit 0ea8130

File tree

10 files changed

+242
-159
lines changed

10 files changed

+242
-159
lines changed

.changeset/bright-rats-mix.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@hey-api/openapi-ts': patch
3+
---
4+
5+
fix: add crash report prompt

.changeset/twenty-numbers-talk.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
---
2-
'@hey-api/client-custom': minor
32
'@hey-api/client-axios': minor
4-
'@hey-api/client-fetch': minor
53
'@hey-api/client-core': minor
4+
'@hey-api/client-custom': minor
5+
'@hey-api/client-fetch': minor
66
'@hey-api/client-next': minor
7-
'@hey-api/client-nuxt': minor
87
---
98

10-
feat: export buildClientParams function
9+
feat: export `buildClientParams` function

packages/openapi-ts-tests/test/openapi-ts.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export default defineConfig(() => {
5151
// 'invalid',
5252
// 'servers-entry.yaml',
5353
// ),
54-
path: path.resolve(__dirname, 'spec', '3.1.x', 'sdk-instance.yaml'),
54+
path: path.resolve(__dirname, 'spec', '3.0.x', 'bug.yaml'),
5555
// path: 'http://localhost:4000/',
5656
// path: 'https://get.heyapi.dev/',
5757
// path: 'https://get.heyapi.dev/hey-api/backend?branch=main&version=1.0.0',
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
openapi: 3.0.3
2+
info:
3+
description: TODO
4+
version: 1
5+
components:
6+
schemas:
7+
Foo:
8+
type: object
9+
properties:
10+
foo:
11+
type: string
12+
bar:
13+
type: object
14+
properties:
15+
baz:
16+
type: boolean
17+
readOnly: true

packages/openapi-ts/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@
9393
"c12": "2.0.1",
9494
"color-support": "1.1.3",
9595
"commander": "13.0.0",
96-
"handlebars": "4.7.8"
96+
"handlebars": "4.7.8",
97+
"open": "10.1.2"
9798
},
9899
"peerDependencies": {
99100
"typescript": "^5.5.3"

packages/openapi-ts/src/error.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
4+
import colors from 'ansi-colors';
5+
6+
import { ensureDirSync } from './generate/utils';
7+
8+
export const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
9+
10+
export class HeyApiError extends Error {
11+
args: ReadonlyArray<unknown>;
12+
event: string;
13+
pluginName: string;
14+
15+
constructor({
16+
args,
17+
error,
18+
event,
19+
name,
20+
pluginName,
21+
}: {
22+
args: unknown[];
23+
error: Error;
24+
event: string;
25+
name: string;
26+
pluginName: string;
27+
}) {
28+
const message = error instanceof Error ? error.message : 'Unknown error';
29+
super(message);
30+
31+
this.args = args;
32+
this.cause = error.cause;
33+
this.event = event;
34+
this.name = name || error.name;
35+
this.pluginName = pluginName;
36+
this.stack = error.stack;
37+
}
38+
}
39+
40+
export const logCrashReport = (error: unknown, logsDir: string): string => {
41+
const logName = `openapi-ts-error-${Date.now()}.log`;
42+
const fullDir = path.resolve(process.cwd(), logsDir);
43+
ensureDirSync(fullDir);
44+
const logPath = path.resolve(fullDir, logName);
45+
46+
let logContent = `[${new Date().toISOString()}] `;
47+
48+
if (error instanceof HeyApiError) {
49+
logContent += `${error.name} during event "${error.event}"\n`;
50+
if (error.pluginName) {
51+
logContent += `Plugin: ${error.pluginName}\n`;
52+
}
53+
logContent += `Arguments: ${JSON.stringify(error.args, null, 2)}\n\n`;
54+
}
55+
56+
const message = error instanceof Error ? error.message : String(error);
57+
const stack = error instanceof Error ? error.stack : undefined;
58+
59+
logContent += `Error: ${message}\n`;
60+
if (stack) {
61+
logContent += `Stack:\n${stack}\n`;
62+
}
63+
64+
fs.writeFileSync(logPath, logContent);
65+
66+
return logPath;
67+
};
68+
69+
export const openGitHubIssueWithCrashReport = async (error: unknown) => {
70+
let body = '';
71+
72+
if (error instanceof HeyApiError) {
73+
if (error.pluginName) {
74+
body += `**Plugin**: \`${error.pluginName}\`\n`;
75+
}
76+
body += `**Event**: \`${error.event}\`\n`;
77+
body += `**Arguments**:\n\`\`\`ts\n${JSON.stringify(error.args, null, 2)}\n\`\`\`\n\n`;
78+
}
79+
80+
const message = error instanceof Error ? error.message : String(error);
81+
const stack = error instanceof Error ? error.stack : undefined;
82+
83+
body += `**Error**: \`${message}\`\n`;
84+
if (stack) {
85+
body += `\n**Stack Trace**:\n\`\`\`\n${stack}\n\`\`\``;
86+
}
87+
88+
const search = new URLSearchParams({
89+
body,
90+
labels: 'bug 🔥',
91+
title: 'Crash Report',
92+
});
93+
94+
const url = `https://github.com/hey-api/openapi-ts/issues/new?${search.toString()}`;
95+
96+
const open = (await import('open')).default;
97+
await open(url);
98+
};
99+
100+
export const printCrashReport = ({
101+
error,
102+
logPath,
103+
}: {
104+
error: unknown;
105+
logPath: string | undefined;
106+
}) => {
107+
process.stdout.write(
108+
`\n🛑 ${colors.cyan('@hey-api/openapi-ts')} ${colors.red('encountered an error.')}` +
109+
`\n\n${colors.red('❗️ Error:')} ${colors.white(typeof error === 'string' ? error : error instanceof Error ? error.message : 'Unknown error')}` +
110+
(logPath
111+
? `\n\n${colors.cyan('📄 Crash log saved to:')} ${colors.gray(logPath)}`
112+
: ''),
113+
);
114+
};
115+
116+
export const shouldReportCrash = async (): Promise<boolean> => {
117+
if (!isInteractive) {
118+
return false;
119+
}
120+
121+
return new Promise((resolve) => {
122+
process.stdout.write(
123+
`${colors.yellow('\n\n📢 Open a GitHub issue with crash details?')} ${colors.yellow('(y/N):')}`,
124+
);
125+
process.stdin.setEncoding('utf8');
126+
process.stdin.once('data', (data: string) => {
127+
resolve(data.trim().toLowerCase() === 'y');
128+
});
129+
});
130+
};

packages/openapi-ts/src/generate/output.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import path from 'node:path';
33
import ts from 'typescript';
44

55
import { compiler } from '../compiler';
6+
import type { Events } from '../ir/context';
67
import { parseIR } from '../ir/parser';
78
import type { IR } from '../ir/types';
89
import { getClientPlugin } from '../plugins/@hey-api/client-core/utils';
@@ -27,6 +28,13 @@ export const generateOutput = async ({ context }: { context: IR.Context }) => {
2728

2829
for (const name of context.config.pluginOrder) {
2930
const plugin = context.config.plugins[name]!;
31+
const _subscribe = context.subscribe.bind(context);
32+
context.subscribe = <T extends keyof Events>(
33+
event: T,
34+
callbackFn: Events[T],
35+
): void => {
36+
_subscribe(event, callbackFn, name);
37+
};
3038
plugin._handler({
3139
context,
3240
plugin: plugin as never,

packages/openapi-ts/src/index.ts

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
import fs from 'node:fs';
2-
import path from 'node:path';
3-
41
import colors from 'ansi-colors';
52
// @ts-expect-error
63
import colorSupport from 'color-support';
74

85
import { createClient as pCreateClient } from './createClient';
9-
import { ensureDirSync } from './generate/utils';
6+
import {
7+
logCrashReport,
8+
openGitHubIssueWithCrashReport,
9+
printCrashReport,
10+
shouldReportCrash,
11+
} from './error';
1012
import { getLogs } from './getLogs';
1113
import { initConfigs } from './initConfigs';
1214
import type { IR } from './ir/types';
@@ -73,20 +75,17 @@ export const createClient = async (
7375
} catch (error) {
7476
const config = configs[0] as Config | undefined;
7577
const dryRun = config ? config.dryRun : resolvedConfig?.dryRun;
76-
77-
// TODO: add setting for log output
78-
if (!dryRun) {
79-
const logs = config?.logs ?? getLogs(resolvedConfig);
80-
if (logs.level !== 'silent' && logs.file) {
81-
const logName = `openapi-ts-error-${Date.now()}.log`;
82-
const logsDir = path.resolve(process.cwd(), logs.path ?? '');
83-
ensureDirSync(logsDir);
84-
const logPath = path.resolve(logsDir, logName);
85-
fs.writeFileSync(logPath, `${error.message}\n${error.stack}`);
86-
console.error(`🔥 Unexpected error occurred. Log saved to ${logPath}`);
78+
const logs = config?.logs ?? getLogs(resolvedConfig);
79+
const shouldLog = !dryRun && logs.level !== 'silent' && logs.file;
80+
81+
if (shouldLog) {
82+
const logPath = logCrashReport(error, logs.path ?? '');
83+
printCrashReport({ error, logPath });
84+
if (await shouldReportCrash()) {
85+
await openGitHubIssueWithCrashReport(error);
8786
}
8887
}
89-
console.error(`🔥 Unexpected error occurred. ${error.message}`);
88+
9089
throw error;
9190
}
9291
};

packages/openapi-ts/src/ir/context.ts

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import path from 'node:path';
22

3+
import { HeyApiError } from '../error';
34
import { TypeScriptFile } from '../generate/files';
45
import type { Config, StringCase } from '../types/config';
56
import type { Files } from '../types/utils';
@@ -27,7 +28,7 @@ interface ContextFile {
2728
path: string;
2829
}
2930

30-
interface Events {
31+
export interface Events {
3132
/**
3233
* Called after parsing.
3334
*/
@@ -59,8 +60,13 @@ interface Events {
5960
server: (args: { server: IR.ServerObject }) => void;
6061
}
6162

63+
type ListenerWithMeta<T extends keyof Events> = {
64+
callbackFn: Events[T];
65+
pluginName: string;
66+
};
67+
6268
type Listeners = {
63-
[T in keyof Events]?: Array<Events[T]>;
69+
[T in keyof Events]?: Array<ListenerWithMeta<T>>;
6470
};
6571

6672
export class IRContext<Spec extends Record<string, any> = any> {
@@ -102,24 +108,28 @@ export class IRContext<Spec extends Record<string, any> = any> {
102108
event: T,
103109
...args: Parameters<Events[T]>
104110
): Promise<void> {
105-
if (!this.listeners[event]) {
106-
return;
107-
}
111+
const eventListeners = this.listeners[event];
108112

109-
await Promise.all(
110-
this.listeners[event].map((callbackFn, index) => {
113+
if (eventListeners) {
114+
for (const listener of eventListeners) {
111115
try {
112-
// @ts-expect-error
113-
const response = callbackFn(...args);
114-
return Promise.resolve(response);
115-
} catch (error) {
116-
console.error(
117-
`🔥 Event broadcast: "${event}"\nindex: ${index}\narguments: ${JSON.stringify(args, null, 2)}`,
116+
await listener.callbackFn(
117+
// @ts-expect-error
118+
...args,
118119
);
119-
throw error;
120+
} catch (error) {
121+
const originalError =
122+
error instanceof Error ? error : new Error(String(error));
123+
throw new HeyApiError({
124+
args,
125+
error: originalError,
126+
event,
127+
name: 'BroadcastError',
128+
pluginName: listener.pluginName,
129+
});
120130
}
121-
}),
122-
);
131+
}
132+
}
123133
}
124134

125135
/**
@@ -192,10 +202,14 @@ export class IRContext<Spec extends Record<string, any> = any> {
192202
public subscribe<T extends keyof Events>(
193203
event: T,
194204
callbackFn: Events[T],
205+
pluginName?: string,
195206
): void {
196207
if (!this.listeners[event]) {
197208
this.listeners[event] = [];
198209
}
199-
this.listeners[event].push(callbackFn);
210+
this.listeners[event].push({
211+
callbackFn,
212+
pluginName: pluginName ?? '',
213+
});
200214
}
201215
}

0 commit comments

Comments
 (0)