Skip to content

Commit b2eee07

Browse files
authored
Merge pull request #7141 from NomicFoundation/project-type-ga-hit
Send selected project type to GA
2 parents 90fbe42 + 802d539 commit b2eee07

File tree

6 files changed

+114
-35
lines changed

6 files changed

+114
-35
lines changed

.changeset/strong-actors-count.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"hardhat": patch
3+
---
4+
5+
Send selected project type to GA

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

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import * as semver from "semver";
2424
import { findClosestHardhatConfig } from "../../config-loading.js";
2525
import { HARDHAT_NAME } from "../../constants.js";
2626
import { getHardhatVersion } from "../../utils/package.js";
27+
import { sendProjectTypeAnalytics } from "../telemetry/analytics/analytics.js";
2728

2829
import {
2930
getDevDependenciesInstallationCommand,
@@ -96,7 +97,10 @@ export async function initHardhat(options?: InitHardhatOptions): Promise<void> {
9697

9798
// Ask the user for the template to use for the project initialization
9899
// if it was not provided, and validate that it exists
99-
const template = await getTemplate(hardhatVersion, options?.template);
100+
const [template, projectTypeAnalyticsPromise] = await getTemplate(
101+
hardhatVersion,
102+
options?.template,
103+
);
100104

101105
// Create the package.json file if it does not exist
102106
// and validate that it is an esm package
@@ -112,7 +116,11 @@ export async function initHardhat(options?: InitHardhatOptions): Promise<void> {
112116

113117
// Print the commands to install the project dependencies
114118
// Run them only if the user opts-in to it
115-
await installProjectDependencies(workspace, template, options?.install);
119+
// Concurrently, await the analytics hit
120+
await Promise.all([
121+
installProjectDependencies(workspace, template, options?.install),
122+
projectTypeAnalyticsPromise,
123+
]);
116124

117125
showStarOnGitHubMessage();
118126
} catch (e) {
@@ -241,26 +249,34 @@ export async function getWorkspace(workspace?: string): Promise<string> {
241249
* NOTE: This function is exported for testing purposes
242250
*
243251
* @param template The name of the template to use for the project initialization.
244-
* @returns
252+
* @returns A tuple with two elements: the template and a promise with the analytics hit.
245253
*/
246254
export async function getTemplate(
247255
hardhatVersion: "hardhat-2" | "hardhat-3",
248256
template?: string,
249-
): Promise<Template> {
257+
): Promise<[Template, Promise<boolean>]> {
250258
const templates = await getTemplates(hardhatVersion);
251259

252260
// Ask the user for the template to use for the project initialization if it was not provided
253261
if (template === undefined) {
254262
template = await promptForTemplate(templates);
255263
}
256264

265+
const projectTypeAnalyticsPromise = sendProjectTypeAnalytics(
266+
hardhatVersion,
267+
template,
268+
);
269+
257270
// Validate that the template exists
258271
for (const t of templates) {
259272
if (t.name === template) {
260-
return t;
273+
return [t, projectTypeAnalyticsPromise];
261274
}
262275
}
263276

277+
// we wait for the GA hit before throwing
278+
await projectTypeAnalyticsPromise;
279+
264280
throw new HardhatError(HardhatError.ERRORS.CORE.GENERAL.TEMPLATE_NOT_FOUND, {
265281
template,
266282
});

v-next/hardhat/src/internal/cli/telemetry/analytics/analytics.ts

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type {
2-
EventNames,
2+
AnalyticsEvent,
33
Payload,
4-
TaskParams,
54
TelemetryConfigPayload,
65
} from "./types.js";
76

@@ -54,23 +53,38 @@ export async function sendTelemetryConfigAnalytics(
5453
}
5554

5655
export async function sendTaskAnalytics(taskId: string[]): Promise<boolean> {
57-
const eventParams: TaskParams = {
58-
task: taskId.join(", "),
56+
const taskAnalyticsEvent: AnalyticsEvent = {
57+
name: "task",
58+
params: {
59+
task: taskId.join(", "),
60+
},
5961
};
6062

61-
return sendAnalytics("task", eventParams);
63+
return sendAnalytics(taskAnalyticsEvent);
6264
}
6365

64-
// Return a boolean for testing purposes to confirm whether analytics were sent based on the consent value and not in CI environments
65-
async function sendAnalytics(
66-
eventName: EventNames,
67-
eventParams: TaskParams,
66+
export async function sendProjectTypeAnalytics(
67+
hardhatVersion: "hardhat-2" | "hardhat-3",
68+
template: string,
6869
): Promise<boolean> {
70+
const initAnalyticsEvent: AnalyticsEvent = {
71+
name: "init",
72+
params: {
73+
hardhatVersion,
74+
template,
75+
},
76+
};
77+
78+
return sendAnalytics(initAnalyticsEvent);
79+
}
80+
81+
// Return a boolean for testing purposes to confirm whether analytics were sent based on the consent value and not in CI environments
82+
async function sendAnalytics(analyticsEvent: AnalyticsEvent): Promise<boolean> {
6983
if (!(await isTelemetryAllowed())) {
7084
return false;
7185
}
7286

73-
const payload = await buildPayload(eventName, eventParams);
87+
const payload = await buildPayload(analyticsEvent);
7488

7589
await createSubprocessToSendAnalytics(payload);
7690

@@ -104,10 +118,7 @@ async function createSubprocessToSendAnalytics(
104118
log("Payload sent to detached subprocess");
105119
}
106120

107-
async function buildPayload(
108-
eventName: EventNames,
109-
eventParams: TaskParams,
110-
): Promise<Payload> {
121+
async function buildPayload(analyticsEvent: AnalyticsEvent): Promise<Payload> {
111122
const clientId = await getAnalyticsClientId();
112123

113124
return {
@@ -121,11 +132,11 @@ async function buildPayload(
121132
},
122133
events: [
123134
{
124-
name: eventName,
135+
name: analyticsEvent.name,
125136
params: {
126137
engagement_time_msec: ENGAGEMENT_TIME_MSEC,
127138
session_id: SESSION_ID,
128-
...eventParams,
139+
...analyticsEvent.params,
129140
},
130141
},
131142
],

v-next/hardhat/src/internal/cli/telemetry/analytics/types.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,20 @@ export interface TelemetryConfigPayload extends BasePayload {
3232
}>;
3333
}
3434

35-
export type EventNames = "task";
36-
37-
export interface TaskParams {
38-
task: string;
39-
}
35+
export type AnalyticsEvent =
36+
| {
37+
name: "task";
38+
params: {
39+
task: string;
40+
};
41+
}
42+
| {
43+
name: "init";
44+
params: {
45+
hardhatVersion: "hardhat-2" | "hardhat-3";
46+
template: string;
47+
};
48+
};
4049

4150
export interface Payload extends BasePayload {
4251
user_properties: {
@@ -54,10 +63,10 @@ export interface Payload extends BasePayload {
5463
};
5564
};
5665
events: Array<{
57-
name: EventNames;
66+
name: AnalyticsEvent["name"];
5867
params: {
5968
engagement_time_msec: string;
6069
session_id: string;
61-
} & TaskParams;
70+
} & AnalyticsEvent["params"];
6271
}>;
6372
}

v-next/hardhat/test/internal/cli/init/init.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ describe("getTemplate", () => {
9090
);
9191
});
9292
it("should return the provided template", async () => {
93-
const template = await getTemplate("hardhat-3", "mocha-ethers");
93+
const [template] = await getTemplate("hardhat-3", "mocha-ethers");
9494
assert.equal(template.name, "mocha-ethers");
9595
});
9696
});
@@ -284,7 +284,7 @@ describe("copyProjectFiles", () => {
284284

285285
describe("when force is true", () => {
286286
it("should copy the template files to the workspace and overwrite existing files", async () => {
287-
const template = await getTemplate("hardhat-3", "mocha-ethers");
287+
const [template] = await getTemplate("hardhat-3", "mocha-ethers");
288288
// Create template files with "some content" in the workspace
289289
const workspaceFiles = template.files.map(
290290
relativeTemplateToWorkspacePath,
@@ -303,7 +303,7 @@ describe("copyProjectFiles", () => {
303303
}
304304
});
305305
it("should copy the .gitignore file correctly", async () => {
306-
const template = await getTemplate("hardhat-3", "mocha-ethers");
306+
const [template] = await getTemplate("hardhat-3", "mocha-ethers");
307307
// Copy the template files to the workspace
308308
await copyProjectFiles(process.cwd(), template, true);
309309
// Check that the .gitignore exists but gitignore does not
@@ -319,7 +319,7 @@ describe("copyProjectFiles", () => {
319319
});
320320
describe("when force is false", () => {
321321
it("should copy the template files to the workspace and NOT overwrite existing files", async () => {
322-
const template = await getTemplate("hardhat-3", "mocha-ethers");
322+
const [template] = await getTemplate("hardhat-3", "mocha-ethers");
323323
// Create template files with "some content" in the workspace
324324
const workspaceFiles = template.files.map(
325325
relativeTemplateToWorkspacePath,
@@ -338,7 +338,7 @@ describe("copyProjectFiles", () => {
338338
}
339339
});
340340
it("should copy the .gitignore file correctly", async () => {
341-
const template = await getTemplate("hardhat-3", "mocha-ethers");
341+
const [template] = await getTemplate("hardhat-3", "mocha-ethers");
342342
// Copy the template files to the workspace
343343
await copyProjectFiles(process.cwd(), template, false);
344344
// Check that the .gitignore exists but gitignore does not
@@ -396,7 +396,7 @@ describe("installProjectDependencies", async () => {
396396
}
397397

398398
it("should not install any template dependencies if the user opts-out of the installation", async () => {
399-
const template = await getTemplate("hardhat-3", "mocha-ethers");
399+
const [template] = await getTemplate("hardhat-3", "mocha-ethers");
400400
await writeUtf8File("package.json", JSON.stringify({ type: "module" }));
401401
await installProjectDependencies(process.cwd(), template, false, false);
402402
assert.ok(!(await exists("node_modules")), "node_modules should not exist");
@@ -412,7 +412,7 @@ describe("installProjectDependencies", async () => {
412412
process.env.GITHUB_HEAD_REF?.startsWith("changeset-release/"),
413413
},
414414
async () => {
415-
const template = await getTemplate("hardhat-3", "mocha-ethers");
415+
const [template] = await getTemplate("hardhat-3", "mocha-ethers");
416416
await writeUtf8File(
417417
"package.json",
418418
JSON.stringify({

v-next/hardhat/test/internal/cli/telemetry/analytics/analytics.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { after, afterEach, before, beforeEach, describe, it } from "node:test";
77
import { readJsonFile, remove } from "@nomicfoundation/hardhat-utils/fs";
88

99
import {
10+
sendProjectTypeAnalytics,
1011
sendTaskAnalytics,
1112
sendTelemetryConfigAnalytics,
1213
} from "../../../../../src/internal/cli/telemetry/analytics/analytics.js";
@@ -133,9 +134,46 @@ describe("analytics", () => {
133134
assert.equal(result.events[0].name, "task");
134135
assert.equal(result.events[0].params.engagement_time_msec, "10000");
135136
assert.notEqual(result.events[0].params.session_id, undefined);
137+
assert(
138+
"task" in result.events[0].params,
139+
"params should have a task field",
140+
);
136141
assert.equal(result.events[0].params.task, "task, subtask");
137142
});
138143

144+
it("should create the correct payload for the init analytics", async () => {
145+
const wasSent = await sendProjectTypeAnalytics(
146+
"hardhat-3",
147+
"mocha-ethers",
148+
);
149+
150+
await checkIfSubprocessWasExecuted(RESULT_FILE_PATH);
151+
152+
const result: Payload = await readJsonFile(RESULT_FILE_PATH);
153+
154+
assert.equal(wasSent, true);
155+
156+
// Check payload properties
157+
assert.notEqual(result.client_id, undefined);
158+
assert.notEqual(result.user_id, undefined);
159+
assert.equal(result.user_properties.projectId.value, "hardhat-project");
160+
assert.equal(
161+
result.user_properties.hardhatVersion.value,
162+
await getHardhatVersion(),
163+
);
164+
assert.notEqual(result.user_properties.operatingSystem.value, undefined);
165+
assert.notEqual(result.user_properties.nodeVersion.value, undefined);
166+
assert.equal(result.events[0].name, "init");
167+
assert.equal(result.events[0].params.engagement_time_msec, "10000");
168+
assert.notEqual(result.events[0].params.session_id, undefined);
169+
assert(
170+
"hardhatVersion" in result.events[0].params,
171+
"params should have a task field",
172+
);
173+
assert.equal(result.events[0].params.hardhatVersion, "hardhat-3");
174+
assert.equal(result.events[0].params.template, "mocha-ethers");
175+
});
176+
139177
it("should not send analytics because the user explicitly opted out of telemetry", async () => {
140178
process.env.HARDHAT_TEST_TELEMETRY_ENABLED = "false";
141179

0 commit comments

Comments
 (0)