Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(turbo-ignore): usage metrics #7897

Merged
merged 1 commit into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/create-turbo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
},
"dependencies": {
"chalk": "4.1.2",
"commander": "^10.0.0",
"commander": "^11.0.0",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updating to use same version across all our CLIs. Nothing breaking is relevant for us:

https://github.com/tj/commander.js/releases/tag/v11.1.0

Copy link
Contributor

@arlyon arlyon Apr 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wish pnpm had workspace-wide package version definitions like in cargo...

"fs-extra": "^11.1.1",
"inquirer": "^8.0.0",
"proxy-agent": "^6.2.2",
Expand Down
54 changes: 50 additions & 4 deletions packages/turbo-ignore/__tests__/ignore.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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);
Expand All @@ -25,6 +26,22 @@ describe("turboIgnore()", () => {
const mockExit = spyExit();
const mockConsole = spyConsole();

const telemetry = new TurboIgnoreTelemetry({
api: "https://example.com",
packageInfo: {
name: "turbo-ignore",
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")
Expand All @@ -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`,
Expand Down Expand Up @@ -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`,
Expand Down Expand Up @@ -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) => {
Expand All @@ -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();
});
});
1 change: 1 addition & 0 deletions packages/turbo-ignore/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
},
"devDependencies": {
"@turbo/eslint-config": "workspace:*",
"@turbo/telemetry": "workspace:*",
"@turbo/test-utils": "workspace:*",
"@turbo/tsconfig": "workspace:*",
"@turbo/types": "workspace:*",
Expand Down
27 changes: 27 additions & 0 deletions packages/turbo-ignore/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,40 @@
#!/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
.name(cliPkg.name)
.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.`
Expand Down Expand Up @@ -41,4 +65,7 @@ turboIgnoreCli
.showHelpAfterError(false)
.action(turboIgnore);

// Add telemetry command to the CLI
withTelemetryCommand(turboIgnoreCli);

turboIgnoreCli.parse();
14 changes: 14 additions & 0 deletions packages/turbo-ignore/src/ignore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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,
Expand Down Expand Up @@ -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}`);
Expand Down Expand Up @@ -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" });
}
});
}
4 changes: 4 additions & 0 deletions packages/turbo-ignore/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { type TurboIgnoreTelemetry } from "@turbo/telemetry";

export type NonFatalErrorKey =
| "MISSING_LOCKFILE"
| "NO_PACKAGE_MANAGER"
Expand All @@ -24,4 +26,6 @@ export interface TurboIgnoreOptions {
fallback?: string;
// The maxBuffer for the child process in KB
maxBuffer?: number;
// The telemetry client
telemetry?: TurboIgnoreTelemetry;
}
5 changes: 3 additions & 2 deletions packages/turbo-telemetry/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -44,6 +45,6 @@
"typescript": "5.3.3"
},
"peerDependencies": {
"commander": "^10.0.0"
"commander": "^11.0.0"
}
}
14 changes: 14 additions & 0 deletions packages/turbo-telemetry/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
}
}
3 changes: 2 additions & 1 deletion packages/turbo-telemetry/src/events/create-turbo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
});
}
}
Expand Down
76 changes: 73 additions & 3 deletions packages/turbo-telemetry/src/events/turbo-ignore.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,81 @@
import { name } from "ci-info";
import { TelemetryClient } from "../client";
import type { Event } from "./types";

const TASK_ALLOWLIST: Readonly<Array<string>> = [
"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(),
});
}
}
}
1 change: 1 addition & 0 deletions packages/turbo-telemetry/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export { withTelemetryCommand } from "./cli";

// Event Classes
export { CreateTurboTelemetry } from "./events/create-turbo";
export { TurboIgnoreTelemetry } from "./events/turbo-ignore";