Skip to content

Commit 26644af

Browse files
feat: add lightweight CLI telemetry (#23)
* feat: add lightweight CLI telemetry * chore: align telemetry naming with PostHog guide * docs: trim telemetry readme note * fix: harden telemetry opt-out handling * refactor: always shutdown telemetry client
1 parent 87fb49e commit 26644af

9 files changed

Lines changed: 434 additions & 15 deletions

File tree

.github/workflows/publish.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ jobs:
2525
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
2626
runs-on: ubuntu-latest
2727
timeout-minutes: 15
28+
env:
29+
CREATE_PRISMA_TELEMETRY_API_KEY: ${{ secrets.CREATE_PRISMA_TELEMETRY_API_KEY }}
30+
CREATE_PRISMA_TELEMETRY_HOST: ${{ vars.CREATE_PRISMA_TELEMETRY_HOST }}
2831
permissions:
2932
contents: read
3033
id-token: write
@@ -160,6 +163,9 @@ jobs:
160163
if: github.event_name == 'push' && startsWith(github.event.head_commit.message, 'chore(release):')
161164
runs-on: ubuntu-latest
162165
timeout-minutes: 15
166+
env:
167+
CREATE_PRISMA_TELEMETRY_API_KEY: ${{ secrets.CREATE_PRISMA_TELEMETRY_API_KEY }}
168+
CREATE_PRISMA_TELEMETRY_HOST: ${{ vars.CREATE_PRISMA_TELEMETRY_HOST }}
163169
permissions:
164170
contents: write
165171
id-token: write

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,10 @@ When add-ons are enabled, `create` prompts for the relevant agent and IDE select
154154
When `postgresql` is selected, `create` can provision Prisma Postgres via `create-db --json` and auto-fill `DATABASE_URL`.
155155
Generated projects also include `db:seed` and configure Prisma's `migrations.seed` hook to run `tsx prisma/seed.ts`.
156156

157+
## Telemetry
158+
159+
Published builds may send anonymous PostHog telemetry for `create` runs to help improve the CLI. It does not include project names, file paths, or database URLs. Disable it with `DO_NOT_TRACK`, `CREATE_PRISMA_DISABLE_TELEMETRY`, or `CREATE_PRISMA_TELEMETRY_DISABLED`.
160+
157161
## Scripts
158162

159163
- `bun run build` - Build to `dist/`

bun.lock

Lines changed: 49 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"execa": "^9.6.1",
5353
"fs-extra": "^11.3.3",
5454
"handlebars": "^4.7.8",
55+
"posthog-node": "4.18.0",
5556
"trpc-cli": "^0.12.4",
5657
"zod": "^4.3.6"
5758
},

src/commands/create.ts

Lines changed: 98 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,25 @@ import {
1717
collectCreateAddonSetupContext,
1818
executeCreateAddonSetupContext,
1919
} from "../tasks/setup-addons";
20+
import {
21+
trackCreateCompleted,
22+
trackCreateFailed,
23+
type CreateTelemetryFailureStage,
24+
} from "../telemetry";
2025
import { getCreatePrismaIntro } from "../ui/branding";
2126

2227
const DEFAULT_PROJECT_NAME = "my-app";
2328
const DEFAULT_TEMPLATE: CreateTemplate = "hono";
2429
const DEFAULT_SCHEMA_PRESET: SchemaPreset = "basic";
2530

31+
type ExecuteCreateContextResult =
32+
| { ok: true }
33+
| {
34+
ok: false;
35+
stage: CreateTelemetryFailureStage;
36+
error?: unknown;
37+
};
38+
2639
function toPackageName(projectName: string): string {
2740
return (
2841
projectName
@@ -148,19 +161,59 @@ async function inspectTargetPath(targetPath: string): Promise<CreateTargetPathSt
148161
}
149162

150163
export async function runCreateCommand(rawInput: CreateCommandInput = {}): Promise<void> {
164+
const startedAt = Date.now();
165+
let input: CreateCommandInput = {};
166+
let context: CreatePromptContext | undefined;
167+
let failureStage: CreateTelemetryFailureStage = "validate_input";
168+
151169
try {
152-
const input = CreateCommandInputSchema.parse(rawInput);
170+
input = CreateCommandInputSchema.parse(rawInput);
153171

154172
intro(getCreatePrismaIntro());
155173

156-
const context = await collectCreateContext(input);
174+
failureStage = "collect_context";
175+
context = await collectCreateContext(input);
157176
if (!context) {
158177
return;
159178
}
160179

161-
await executeCreateContext(context);
180+
failureStage = "unknown";
181+
const executionResult = await executeCreateContext(context);
182+
if (!executionResult.ok) {
183+
if (executionResult.error) {
184+
cancel(
185+
`Create command failed: ${
186+
executionResult.error instanceof Error
187+
? executionResult.error.message
188+
: String(executionResult.error)
189+
}`,
190+
);
191+
}
192+
193+
await trackCreateFailed({
194+
input,
195+
context,
196+
durationMs: Date.now() - startedAt,
197+
error: executionResult.error,
198+
stage: executionResult.stage,
199+
});
200+
return;
201+
}
202+
203+
await trackCreateCompleted({
204+
input,
205+
context,
206+
durationMs: Date.now() - startedAt,
207+
});
162208
} catch (error) {
163209
cancel(`Create command failed: ${error instanceof Error ? error.message : String(error)}`);
210+
await trackCreateFailed({
211+
input,
212+
context,
213+
durationMs: Date.now() - startedAt,
214+
error,
215+
stage: failureStage,
216+
});
164217
}
165218
}
166219

@@ -230,7 +283,9 @@ async function collectCreateContext(
230283
};
231284
}
232285

233-
async function executeCreateContext(context: CreatePromptContext): Promise<void> {
286+
async function executeCreateContext(
287+
context: CreatePromptContext,
288+
): Promise<ExecuteCreateContextResult> {
234289
const scaffoldSpinner = spinner();
235290
scaffoldSpinner.start(`Scaffolding ${context.template} project...`);
236291
try {
@@ -245,8 +300,11 @@ async function executeCreateContext(context: CreatePromptContext): Promise<void>
245300
scaffoldSpinner.stop("Project files scaffolded.");
246301
} catch (error) {
247302
scaffoldSpinner.stop("Could not scaffold project files.");
248-
cancel(error instanceof Error ? error.message : String(error));
249-
return;
303+
return {
304+
ok: false,
305+
stage: "scaffold_template",
306+
error,
307+
};
250308
}
251309

252310
if (
@@ -261,17 +319,42 @@ async function executeCreateContext(context: CreatePromptContext): Promise<void>
261319

262320
const cdStep = `- cd ${formatPathForDisplay(context.targetDirectory)}`;
263321
if (context.addonSetupContext) {
264-
await executeCreateAddonSetupContext({
265-
context: context.addonSetupContext,
266-
packageManager: context.prismaSetupContext.packageManager,
322+
try {
323+
await executeCreateAddonSetupContext({
324+
context: context.addonSetupContext,
325+
packageManager: context.prismaSetupContext.packageManager,
326+
projectDir: context.targetDirectory,
327+
verbose: context.prismaSetupContext.verbose,
328+
});
329+
} catch (error) {
330+
return {
331+
ok: false,
332+
stage: "addons",
333+
error,
334+
};
335+
}
336+
}
337+
338+
try {
339+
const prismaSetupResult = await executePrismaSetupContext(context.prismaSetupContext, {
340+
prependNextSteps: [cdStep],
267341
projectDir: context.targetDirectory,
268-
verbose: context.prismaSetupContext.verbose,
342+
includeDevNextStep: true,
269343
});
344+
345+
if (!prismaSetupResult) {
346+
return {
347+
ok: false,
348+
stage: "prisma_setup",
349+
};
350+
}
351+
} catch (error) {
352+
return {
353+
ok: false,
354+
stage: "prisma_setup",
355+
error,
356+
};
270357
}
271358

272-
await executePrismaSetupContext(context.prismaSetupContext, {
273-
prependNextSteps: [cdStep],
274-
projectDir: context.targetDirectory,
275-
includeDevNextStep: true,
276-
});
359+
return { ok: true };
277360
}

0 commit comments

Comments
 (0)