Skip to content

Commit 9aac754

Browse files
Implement logic to ask for telemetry consent #5501 (#5503)
1 parent cbc88d2 commit 9aac754

File tree

3 files changed

+141
-0
lines changed

3 files changed

+141
-0
lines changed

v-next/hardhat/src/internal/cli/main.ts

+4
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { printErrorMessages } from "./error-handler.js";
3838
import { getGlobalHelpString } from "./helpers/getGlobalHelpString.js";
3939
import { getHelpString } from "./helpers/getHelpString.js";
4040
import { initHardhat } from "./init/init.js";
41+
import { getTelemetryConsent } from "./telemetry/telemetry-consent.js";
4142
import { printVersionMessage } from "./version.js";
4243

4344
export async function main(
@@ -64,6 +65,9 @@ export async function main(
6465
return await initHardhat();
6566
}
6667

68+
// TODO: the consent will be enabled in the other PRs related to telemetry
69+
const _telemetryConsent = await getTelemetryConsent();
70+
6771
if (builtinGlobalOptions.configPath === undefined) {
6872
builtinGlobalOptions.configPath = await resolveHardhatConfigPath();
6973
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* Display a confirmation prompt to the user. The prompt will be canceled if no response is given within a specified time limit.
3+
* @param name Used as the key for the answer on the returned values (answers) object.
4+
* @param message The message to display when the prompt is rendered in the terminal.
5+
* @param timeoutMilliseconds After how much time the prompt will be cancelled if no answer is given.
6+
* @returns True or false based on the user input, or undefined if the prompt times out.
7+
*/
8+
export async function confirmationPromptWithTimeout(
9+
name: string,
10+
message: string,
11+
timeoutMilliseconds: number = 10_000,
12+
): Promise<boolean | undefined> {
13+
try {
14+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- The types in the module "enquirer" are not properly defined
15+
const { default: enquirer } = (await import("enquirer")) as any;
16+
17+
const prompt = new enquirer.prompts.Confirm(
18+
createConfirmationPrompt(name, message),
19+
);
20+
21+
// The timeout is a safety measure in case Hardhat is executed in a CI or another non-interactive environment and we do not detect it.
22+
// Instead of blocking the process indefinitely, we abort the prompt after a while.
23+
let timeout;
24+
const timeoutPromise = new Promise((resolve) => {
25+
timeout = setTimeout(resolve, timeoutMilliseconds);
26+
});
27+
28+
const result: boolean | undefined = await Promise.race([
29+
prompt.run(),
30+
timeoutPromise,
31+
]);
32+
33+
clearTimeout(timeout);
34+
35+
if (result === undefined) {
36+
await prompt.cancel();
37+
return undefined;
38+
}
39+
40+
return result;
41+
} catch (e) {
42+
if (e === "") {
43+
// If the user cancels the prompt, we quit
44+
return undefined;
45+
}
46+
47+
throw e;
48+
}
49+
}
50+
51+
function createConfirmationPrompt(name: string, message: string) {
52+
return {
53+
type: "confirm",
54+
name,
55+
message,
56+
initial: "y",
57+
default: "(Y/n)",
58+
};
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import path from "node:path";
2+
3+
import { getConfigDir } from "@ignored/hardhat-vnext-core/global-dir";
4+
import { isCi } from "@ignored/hardhat-vnext-utils/ci";
5+
import {
6+
exists,
7+
readJsonFile,
8+
writeJsonFile,
9+
} from "@ignored/hardhat-vnext-utils/fs";
10+
11+
import { confirmationPromptWithTimeout } from "../prompt/prompt.js";
12+
13+
interface TelemetryConsent {
14+
consent: boolean;
15+
}
16+
17+
/**
18+
* Get the user's telemetry consent. If already provided, returns the answer.
19+
* If not, prompts the user.
20+
* Consent is only asked in interactive environments.
21+
* @returns True if the user consents to telemetry, false otherwise.
22+
*/
23+
export async function getTelemetryConsent(): Promise<boolean> {
24+
if (canTelemetryBeEnabled() === false) {
25+
return false;
26+
}
27+
28+
const telemetryConsentFilePath = await getTelemetryConsentFilePath();
29+
30+
if (await exists(telemetryConsentFilePath)) {
31+
// Telemetry consent was already provided, hence return the answer
32+
return (await readJsonFile<TelemetryConsent>(telemetryConsentFilePath))
33+
.consent;
34+
}
35+
36+
// Telemetry consent not provided yet, ask for it
37+
return requestTelemetryConsent();
38+
}
39+
40+
function canTelemetryBeEnabled(): boolean {
41+
return (
42+
!isCi() &&
43+
process.stdout.isTTY === true &&
44+
process.env.HARDHAT_DISABLE_TELEMETRY_PROMPT !== "true"
45+
);
46+
}
47+
48+
async function getTelemetryConsentFilePath() {
49+
const configDir = await getConfigDir();
50+
return path.join(configDir, "telemetry-consent.json");
51+
}
52+
53+
async function requestTelemetryConsent(): Promise<boolean> {
54+
const consent = await confirmTelemetryConsent();
55+
56+
if (consent === undefined) {
57+
return false;
58+
}
59+
60+
// Store user's consent choice
61+
await writeJsonFile(await getTelemetryConsentFilePath(), { consent });
62+
63+
// TODO: this will be enabled in a following PR as soon as the function to send telemetry is implemented
64+
// const subprocessFilePath = path.join(
65+
// path.dirname(fileURLToPath(import.meta.url)),
66+
// "report-telemetry-consent.js",
67+
// );
68+
// await spawnDetachedSubProcess(subprocessFilePath, [consent ? "yes" : "no"]);
69+
70+
return consent;
71+
}
72+
73+
async function confirmTelemetryConsent(): Promise<boolean | undefined> {
74+
return confirmationPromptWithTimeout(
75+
"telemetryConsent",
76+
"Help us improve Hardhat with anonymous crash reports & basic usage data?",
77+
);
78+
}

0 commit comments

Comments
 (0)