From f6255e739296097aff3d000288e3c42c2fc65244 Mon Sep 17 00:00:00 2001 From: tknickman Date: Wed, 3 Apr 2024 11:14:19 -0400 Subject: [PATCH] feat(turbo-ignore): usage metrics --- packages/create-turbo/package.json | 2 +- .../turbo-ignore/__tests__/ignore.test.ts | 54 ++++++++++++- packages/turbo-ignore/package.json | 1 + packages/turbo-ignore/src/cli.ts | 27 +++++++ packages/turbo-ignore/src/ignore.ts | 14 ++++ packages/turbo-ignore/src/types.ts | 4 + packages/turbo-telemetry/package.json | 5 +- packages/turbo-telemetry/src/client.ts | 14 ++++ .../src/events/create-turbo.ts | 3 +- .../src/events/turbo-ignore.ts | 76 ++++++++++++++++++- packages/turbo-telemetry/src/index.ts | 1 + pnpm-lock.yaml | 20 +++-- 12 files changed, 205 insertions(+), 16 deletions(-) diff --git a/packages/create-turbo/package.json b/packages/create-turbo/package.json index 0bfd4588c2d8bf..f5ca9047fe554c 100644 --- a/packages/create-turbo/package.json +++ b/packages/create-turbo/package.json @@ -24,7 +24,7 @@ }, "dependencies": { "chalk": "4.1.2", - "commander": "^10.0.0", + "commander": "^11.0.0", "fs-extra": "^11.1.1", "inquirer": "^8.0.0", "proxy-agent": "^6.2.2", diff --git a/packages/turbo-ignore/__tests__/ignore.test.ts b/packages/turbo-ignore/__tests__/ignore.test.ts index 20624067af49c7..c15bd053162d71 100644 --- a/packages/turbo-ignore/__tests__/ignore.test.ts +++ b/packages/turbo-ignore/__tests__/ignore.test.ts @@ -1,4 +1,4 @@ -// eslint-disable-next-line camelcase +// eslint-disable-next-line camelcase -- This is a test file import child_process, { type ChildProcess, type ExecException, @@ -11,6 +11,7 @@ import { validateLogs, } from "@turbo/test-utils"; import { turboIgnore } from "../src/ignore"; +import { TurboIgnoreTelemetry, TelemetryConfig } from "@turbo/telemetry"; function expectBuild(mockExit: SpyExit) { expect(mockExit.exit).toHaveBeenCalledWith(1); @@ -25,6 +26,22 @@ describe("turboIgnore()", () => { const mockExit = spyExit(); const mockConsole = spyConsole(); + const telemetry = new TurboIgnoreTelemetry({ + api: "https://example.com", + packageInfo: { + name: "create-turbo", + version: "1.0.0", + }, + config: new TelemetryConfig({ + configPath: "test-config-path", + config: { + telemetry_enabled: false, + telemetry_id: "telemetry-test-id", + telemetry_salt: "telemetry-salt", + }, + }), + }); + it("throws error and allows build when exec fails", () => { const mockExec = jest .spyOn(child_process, "exec") @@ -39,7 +56,7 @@ describe("turboIgnore()", () => { return {} as unknown as ChildProcess; }); - turboIgnore("test-workspace", {}); + turboIgnore("test-workspace", { telemetry }); expect(mockExec).toHaveBeenCalledWith( `npx turbo run build --filter="test-workspace...[HEAD^]" --dry=json`, @@ -117,7 +134,7 @@ describe("turboIgnore()", () => { return {} as unknown as ChildProcess; }); - turboIgnore("test-workspace", {}); + turboIgnore("test-workspace", { telemetry }); expect(mockExec).toHaveBeenCalledWith( `npx turbo run build --filter="test-workspace...[too-far-back]" --dry=json`, @@ -607,7 +624,7 @@ describe("turboIgnore()", () => { mockExec.mockRestore(); }); - it("passes max buffer to turbo exectuion", () => { + it("passes max buffer to turbo execution", () => { const mockExec = jest .spyOn(child_process, "exec") .mockImplementation((command, options, callback) => { @@ -631,4 +648,33 @@ describe("turboIgnore()", () => { mockExec.mockRestore(); }); + + it("runs with telemetry", () => { + const mockExec = jest + .spyOn(child_process, "exec") + .mockImplementation((command, options, callback) => { + if (callback) { + return callback( + null, + '{"packages": [],"tasks":[]}', + "stderr" + ) as unknown as ChildProcess; + } + return {} as unknown as ChildProcess; + }); + + turboIgnore(undefined, { + directory: "__fixtures__/app", + maxBuffer: 1024, + telemetry, + }); + + expect(mockExec).toHaveBeenCalledWith( + `npx turbo run build --filter="test-app...[HEAD^]" --dry=json`, + expect.objectContaining({ maxBuffer: 1024 }), + expect.anything() + ); + + mockExec.mockRestore(); + }); }); diff --git a/packages/turbo-ignore/package.json b/packages/turbo-ignore/package.json index 42e99f02ccb9e0..65ab5241ea1fea 100644 --- a/packages/turbo-ignore/package.json +++ b/packages/turbo-ignore/package.json @@ -27,6 +27,7 @@ }, "devDependencies": { "@turbo/eslint-config": "workspace:*", + "@turbo/telemetry": "workspace:*", "@turbo/test-utils": "workspace:*", "@turbo/tsconfig": "workspace:*", "@turbo/types": "workspace:*", diff --git a/packages/turbo-ignore/src/cli.ts b/packages/turbo-ignore/src/cli.ts index 4b17b284db0298..748f4ea8d231a8 100755 --- a/packages/turbo-ignore/src/cli.ts +++ b/packages/turbo-ignore/src/cli.ts @@ -1,9 +1,17 @@ #!/usr/bin/env node import { Command, Option } from "commander"; +import { + type TurboIgnoreTelemetry, + initTelemetry, + withTelemetryCommand, +} from "@turbo/telemetry"; import cliPkg from "../package.json"; import { turboIgnore } from "./ignore"; +// Global telemetry client +let telemetryClient: TurboIgnoreTelemetry | undefined; + const turboIgnoreCli = new Command(); turboIgnoreCli @@ -11,6 +19,22 @@ turboIgnoreCli .description( "Only proceed with deployment if the workspace or any of its dependencies have changed" ) + .hook("preAction", async (_, thisAction) => { + const { telemetry } = await initTelemetry<"turbo-ignore">({ + packageInfo: { + name: "turbo-ignore", + version: cliPkg.version, + }, + }); + // inject telemetry into the action as an option + thisAction.addOption( + new Option("--telemetry").default(telemetry).hideHelp() + ); + telemetryClient = telemetry; + }) + .hook("postAction", async () => { + await telemetryClient?.close(); + }) .argument( "[workspace]", `The workspace being deployed. If [workspace] is not provided, it will be inferred from the "name" field of the "package.json" located at the current working directory.` @@ -41,4 +65,7 @@ turboIgnoreCli .showHelpAfterError(false) .action(turboIgnore); +// Add telemetry command to the CLI +withTelemetryCommand(turboIgnoreCli); + turboIgnoreCli.parse(); diff --git a/packages/turbo-ignore/src/ignore.ts b/packages/turbo-ignore/src/ignore.ts index 0cb94d1b36fb92..07949ed89f1898 100644 --- a/packages/turbo-ignore/src/ignore.ts +++ b/packages/turbo-ignore/src/ignore.ts @@ -11,6 +11,13 @@ import { shouldWarn } from "./errors"; import type { TurboIgnoreArg, TurboIgnoreOptions } from "./types"; import { checkCommit } from "./checkCommit"; +function trackOptions(opts: TurboIgnoreOptions) { + opts.telemetry?.trackOptionTask(opts.task); + opts.telemetry?.trackOptionFallback(opts.fallback); + opts.telemetry?.trackOptionDirectory(opts.directory); + opts.telemetry?.trackOptionMaxBuffer(opts.maxBuffer); +} + function ignoreBuild() { log("⏭ Ignoring the change"); return process.exit(0); @@ -25,6 +32,10 @@ export function turboIgnore( workspaceArg: TurboIgnoreArg, opts: TurboIgnoreOptions ) { + opts.telemetry?.trackCommandStatus({ command: "ignore", status: "start" }); + opts.telemetry?.trackArgumentWorkspace(workspaceArg !== undefined); + trackOptions(opts); + const inputs = { workspace: workspaceArg, ...opts, @@ -108,6 +119,7 @@ export function turboIgnore( if (err) { const { level, code, message } = shouldWarn({ err: err.message }); if (level === "warn") { + opts.telemetry?.trackCommandWarning(message); warn(message); } else { error(`${code}: ${err.message}`); @@ -142,6 +154,8 @@ export function turboIgnore( error(`Failed to parse JSON output from \`${command}\`.`); error(e); return continueBuild(); + } finally { + opts.telemetry?.trackCommandStatus({ command: "ignore", status: "end" }); } }); } diff --git a/packages/turbo-ignore/src/types.ts b/packages/turbo-ignore/src/types.ts index 7dec8fd2e76ae7..bff6fe46925ba5 100644 --- a/packages/turbo-ignore/src/types.ts +++ b/packages/turbo-ignore/src/types.ts @@ -1,3 +1,5 @@ +import { type TurboIgnoreTelemetry } from "@turbo/telemetry"; + export type NonFatalErrorKey = | "MISSING_LOCKFILE" | "NO_PACKAGE_MANAGER" @@ -24,4 +26,6 @@ export interface TurboIgnoreOptions { fallback?: string; // The maxBuffer for the child process in KB maxBuffer?: number; + // The telemetry client + telemetry?: TurboIgnoreTelemetry; } diff --git a/packages/turbo-telemetry/package.json b/packages/turbo-telemetry/package.json index 1e72befc681263..b5b5c3f2744767 100644 --- a/packages/turbo-telemetry/package.json +++ b/packages/turbo-telemetry/package.json @@ -26,15 +26,16 @@ }, "dependencies": { "chalk": "^4.1.2", + "ci-info": "^4.0.0", "got": "^11.8.6", "uuid": "^9.0.0" }, "devDependencies": { "@turbo/eslint-config": "workspace:*", "@turbo/test-utils": "workspace:*", - "@turbo/utils": "workspace:*", "@turbo/tsconfig": "workspace:*", "@turbo/types": "workspace:*", + "@turbo/utils": "workspace:*", "@types/jest": "^27.4.0", "@types/node": "^20.11.30", "@types/uuid": "^9.0.0", @@ -44,6 +45,6 @@ "typescript": "5.3.3" }, "peerDependencies": { - "commander": "^10.0.0" + "commander": "^11.0.0" } } diff --git a/packages/turbo-telemetry/src/client.ts b/packages/turbo-telemetry/src/client.ts index dcda0166600f15..f8a87702f853a2 100644 --- a/packages/turbo-telemetry/src/client.ts +++ b/packages/turbo-telemetry/src/client.ts @@ -191,4 +191,18 @@ export class TelemetryClient { value: status, }); } + + trackCommandWarning(warning: string): Event | undefined { + return this.track({ + key: "warning", + value: warning, + }); + } + + trackCommandError(error: string): Event | undefined { + return this.track({ + key: "error", + value: error, + }); + } } diff --git a/packages/turbo-telemetry/src/events/create-turbo.ts b/packages/turbo-telemetry/src/events/create-turbo.ts index c7c6d71e0ecb17..ed13ea909d3358 100644 --- a/packages/turbo-telemetry/src/events/create-turbo.ts +++ b/packages/turbo-telemetry/src/events/create-turbo.ts @@ -56,11 +56,12 @@ export class CreateTurboTelemetry extends TelemetryClient { } } + // only track that the argument was provided, not what it was trackArgumentDirectory(provided: boolean): Event | undefined { if (provided) { return this.trackCliArgument({ argument: "project_directory", - value: provided.toString(), + value: "provided", }); } } diff --git a/packages/turbo-telemetry/src/events/turbo-ignore.ts b/packages/turbo-telemetry/src/events/turbo-ignore.ts index 58bc2a9b5aa4b4..25beb46e4e6a6e 100644 --- a/packages/turbo-telemetry/src/events/turbo-ignore.ts +++ b/packages/turbo-telemetry/src/events/turbo-ignore.ts @@ -1,11 +1,81 @@ +import { name } from "ci-info"; import { TelemetryClient } from "../client"; import type { Event } from "./types"; +const TASK_ALLOWLIST: Readonly> = [ + "build", + "test", + "lint", + "typecheck", + "checktypes", + "check-types", + "type-check", + "check", +] as const; + export class TurboIgnoreTelemetry extends TelemetryClient { - trackExecutionEnv(): Event | undefined { + trackCI(): Event | undefined { return this.track({ - key: "execution_env", - value: process.env.VERCEL === "1" ? "vercel" : "local", + key: "ci", + value: name ?? "unknown", }); } + + /** + * Track the workspace argument if it's provided. + * We only track if it's provided, not what it was + */ + trackArgumentWorkspace(provided: boolean): Event | undefined { + if (provided) { + return this.trackCliArgument({ + argument: "workspace", + value: "provided", + }); + } + } + + /** + * Track the task option if it's provided. + * We only track the exact task name if it's in the allowlist + * Otherwise, we track it as "other" + */ + trackOptionTask(value: string | undefined): Event | undefined { + if (value) { + return this.trackCliOption({ + option: "task", + value: TASK_ALLOWLIST.includes(value) ? value : "other", + }); + } + } + + trackOptionFallback(value: string | undefined): Event | undefined { + if (value) { + return this.trackCliOption({ + option: "fallback", + value, + }); + } + } + + /** + * Track the directory argument if it's provided. + * We only track if it's provided, not what it was + */ + trackOptionDirectory(value: string | undefined): Event | undefined { + if (value) { + return this.trackCliOption({ + option: "directory", + value: "custom", + }); + } + } + + trackOptionMaxBuffer(value: number | undefined): Event | undefined { + if (value !== undefined) { + return this.trackCliOption({ + option: "max_buffer", + value: value.toString(), + }); + } + } } diff --git a/packages/turbo-telemetry/src/index.ts b/packages/turbo-telemetry/src/index.ts index a3eb666d642b67..e42bb9634656bf 100644 --- a/packages/turbo-telemetry/src/index.ts +++ b/packages/turbo-telemetry/src/index.ts @@ -5,3 +5,4 @@ export { withTelemetryCommand } from "./cli"; // Event Classes export { CreateTurboTelemetry } from "./events/create-turbo"; +export { TurboIgnoreTelemetry } from "./events/turbo-ignore"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 708a363dc8445e..c84cc1208524b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -170,8 +170,8 @@ importers: specifier: 4.1.2 version: 4.1.2 commander: - specifier: ^10.0.0 - version: 10.0.0 + specifier: ^11.0.0 + version: 11.0.0 fs-extra: specifier: ^11.1.1 version: 11.1.1 @@ -625,6 +625,9 @@ importers: '@turbo/eslint-config': specifier: workspace:* version: link:../eslint-config + '@turbo/telemetry': + specifier: workspace:* + version: link:../turbo-telemetry '@turbo/test-utils': specifier: workspace:* version: link:../turbo-test-utils @@ -682,9 +685,12 @@ importers: chalk: specifier: ^4.1.2 version: 4.1.2 + ci-info: + specifier: ^4.0.0 + version: 4.0.0 commander: - specifier: ^10.0.0 - version: 10.0.0 + specifier: ^11.0.0 + version: 11.0.0 got: specifier: ^11.8.6 version: 11.8.6 @@ -4683,6 +4689,11 @@ packages: resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==} engines: {node: '>=8'} + /ci-info@4.0.0: + resolution: {integrity: sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==} + engines: {node: '>=8'} + dev: false + /cjs-module-lexer@1.2.2: resolution: {integrity: sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==} @@ -4841,7 +4852,6 @@ packages: /commander@11.0.0: resolution: {integrity: sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==} engines: {node: '>=16'} - dev: true /commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}