-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathfeedback.ts
More file actions
175 lines (162 loc) · 5.13 KB
/
Copy pathfeedback.ts
File metadata and controls
175 lines (162 loc) · 5.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
import { getCliVersion } from "../lib/version";
import { CliError, usageError } from "../shell/errors";
import type { CommandSuccess } from "../shell/output";
import type { CommandContext } from "../shell/runtime";
import type { FeedbackContext, FeedbackResult } from "../types/feedback";
const DEFAULT_FEEDBACK_ENDPOINT =
"https://hiieirp2pwqnjvq9axzyg6d0.fra.prisma.build/feedback";
// Feedback must never feel slower than the thought it carries; a service
// that cannot answer quickly is treated as unavailable.
const FEEDBACK_TIMEOUT_MS = 3_000;
// Mirrors the feedback service's own limits so refusals happen before the
// network round trip.
const MAX_MESSAGE_LENGTH = 4_000;
const MAX_EMAIL_LENGTH = 320;
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
export interface FeedbackFlags {
email?: string;
}
export async function runFeedback(
context: CommandContext,
messageArg: string,
flags: FeedbackFlags,
): Promise<CommandSuccess<FeedbackResult>> {
const message = messageArg.trim();
if (!message) {
throw usageError(
"Feedback message required",
"The message argument is empty.",
"Pass a non-empty message.",
['prisma-cli feedback "the deploy flow is great"'],
);
}
if (message.length > MAX_MESSAGE_LENGTH) {
throw usageError(
"Feedback message too long",
`The message is ${message.length} characters; the limit is ${MAX_MESSAGE_LENGTH}.`,
"Shorten the message.",
);
}
const email = flags.email?.trim();
if (
email !== undefined &&
(email.length > MAX_EMAIL_LENGTH || !EMAIL_PATTERN.test(email))
) {
throw usageError(
"Invalid email",
`"${flags.email}" is not a valid email address of at most ${MAX_EMAIL_LENGTH} characters.`,
"Pass a valid address with --email, or drop the flag to stay anonymous.",
['prisma-cli feedback "please add X" --email you@example.com'],
);
}
const feedbackContext: FeedbackContext = {
cliVersion: getCliVersion(),
nodeVersion: process.version,
platform: process.platform,
arch: process.arch,
};
const endpoint =
context.runtime.env.PRISMA_CLI_FEEDBACK_URL || DEFAULT_FEEDBACK_ENDPOINT;
const id = await postFeedback(context, endpoint, {
message,
...(email ? { email } : {}),
meta: { ...feedbackContext },
});
return {
command: "feedback",
result: {
id,
email: email ?? null,
context: feedbackContext,
},
warnings: [],
nextSteps: [],
};
}
async function postFeedback(
context: CommandContext,
endpoint: string,
body: Record<string, unknown>,
): Promise<string | null> {
let response: Response;
try {
response = await fetch(endpoint, {
method: "POST",
headers: {
"content-type": "application/json",
"user-agent": `prisma-cli/${getCliVersion()}`,
},
body: JSON.stringify(body),
signal: AbortSignal.any([
context.runtime.signal,
AbortSignal.timeout(FEEDBACK_TIMEOUT_MS),
]),
});
} catch (error) {
if (context.runtime.signal.aborted) {
throw error;
}
const detail =
error instanceof Error && error.name === "TimeoutError"
? TIMEOUT_DETAIL
: `The feedback service could not be reached${error instanceof Error && error.cause instanceof Error ? ` (${error.cause.message})` : ""}.`;
throw feedbackSendFailed(detail);
}
if (!response.ok) {
throw feedbackSendFailed(
`The feedback service responded with HTTP ${response.status}${await readServiceError(context, response)}.`,
);
}
// The body read runs under the same abort signal as the request, so a
// stalled response or a user cancellation here must not be mistaken for a
// fully received non-JSON body.
let payload: { id?: unknown } | null;
try {
payload = (await response.json()) as { id?: unknown } | null;
} catch (error) {
if (context.runtime.signal.aborted) {
throw error;
}
if (!(error instanceof SyntaxError)) {
throw feedbackSendFailed(
error instanceof Error && error.name === "TimeoutError"
? TIMEOUT_DETAIL
: "The feedback service response could not be read.",
);
}
// The body arrived but was not JSON; the submission itself succeeded.
payload = null;
}
return typeof payload?.id === "string" ? payload.id : null;
}
const TIMEOUT_DETAIL = `The feedback service did not answer within ${FEEDBACK_TIMEOUT_MS / 1000} seconds.`;
async function readServiceError(
context: CommandContext,
response: Response,
): Promise<string> {
let payload: { error?: { message?: unknown } } | null;
try {
payload = (await response.json()) as {
error?: { message?: unknown };
} | null;
} catch (error) {
if (context.runtime.signal.aborted) {
throw error;
}
return "";
}
return typeof payload?.error?.message === "string"
? ` (${payload.error.message})`
: "";
}
function feedbackSendFailed(detail: string): CliError {
return new CliError({
code: "FEEDBACK_SEND_FAILED",
domain: "cli",
summary: "Feedback could not be delivered",
why: detail,
fix: "Check your network and rerun the command.",
exitCode: 1,
nextSteps: [],
});
}