From 837e28961f34327192fc9ad917d8139ebed2a90e Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Thu, 8 Oct 2026 13:05:17 +0530 Subject: [PATCH 1/5] fix: replace PostHog SDK with native fetch Signed-off-by: Aman Varshney --- bun.lock | 7 -- package.json | 3 +- src/telemetry/client.ts | 56 ++++------- src/telemetry/create.ts | 3 - tests/install.test.ts | 2 +- tests/telemetry-client.test.ts | 169 +++++++++++++++++++++++++++++---- 6 files changed, 172 insertions(+), 68 deletions(-) diff --git a/bun.lock b/bun.lock index 2380245..4d26afc 100644 --- a/bun.lock +++ b/bun.lock @@ -10,7 +10,6 @@ "effect": "4.0.1", "execa": "^10.0.1", "handlebars": "^4.7.10", - "posthog-node": "^5.55.0", }, "devDependencies": { "@prisma/dev": "0.25.2", @@ -118,10 +117,6 @@ "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.87.0", "", { "os": "win32", "cpu": "x64" }, "sha512-k+m/EShyXE29J/r1fjk0HlAzhN5JhIvj044oSJkXTEkCLrWZQgXb/3r4q5LTw/eaKW/4GuMXG2xmiJs3s5rPNw=="], - "@posthog/core": ["@posthog/core@1.57.0", "", { "dependencies": { "@posthog/types": "^1.415.0" } }, "sha512-ZpIWCSkog0v4whufAXetagBsVLqE1Zjl5DsHC2squTy8nS/vqZ9W5JvWkJFgHK8hk5x1gdwMoXY7FK2S0YRT1Q=="], - - "@posthog/types": ["@posthog/types@1.415.1", "", {}, "sha512-7Qs2wcqipl8KcFpIkIKE1Wcj1djX4dEB3cfq907HrNpKK7/xKh3U4N4bkOD6h9+8j3SxWWrj9JNpE8CZXsE35w=="], - "@prisma/debug": ["@prisma/debug@7.2.0", "", {}, "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw=="], "@prisma/dev": ["@prisma/dev@0.25.2", "", { "dependencies": { "@electric-sql/pglite": "0.4.3", "@electric-sql/pglite-socket": "0.1.3", "@electric-sql/pglite-tools": "0.3.3", "@prisma/get-platform": "7.2.0", "@prisma/query-plan-executor": "7.2.0", "@prisma/streams-local": "0.1.11", "find-my-way": "9.7.0", "foreground-child": "3.3.1", "get-port-please": "3.2.0", "pathe": "2.0.3", "proper-lockfile": "4.1.2", "remeda": "2.33.4", "std-env": "3.10.0", "valibot": "1.4.2", "zeptomatch": "2.1.0" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-igVDlzGSEd/bGutlkh3fXHqXlOkaIHSt+Vpjdo0BrxJ1/wAlDSyntKLqKVNGr2zy22bLVO3majebWZUKINXwdA=="], @@ -528,8 +523,6 @@ "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], - "posthog-node": ["posthog-node@5.55.0", "", { "dependencies": { "@posthog/core": "^1.55.2" }, "peerDependencies": { "rxjs": "^7.0.0" }, "optionalPeers": ["rxjs"] }, "sha512-8dFtlDTqs0CKGZ8t4mUnk4NdShAgzru3B9D0MfM/NTBzlRyAjctivzWINLtMLRQxYjvgBohxy7RTwiQf4mPycQ=="], - "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], diff --git a/package.json b/package.json index a7dd360..46aad6c 100644 --- a/package.json +++ b/package.json @@ -55,8 +55,7 @@ "@effect/platform-node-shared": "4.0.1", "effect": "4.0.1", "execa": "^10.0.1", - "handlebars": "^4.7.10", - "posthog-node": "^5.55.0" + "handlebars": "^4.7.10" }, "devDependencies": { "@prisma/dev": "0.25.2", diff --git a/src/telemetry/client.ts b/src/telemetry/client.ts index 0a27046..939e663 100644 --- a/src/telemetry/client.ts +++ b/src/telemetry/client.ts @@ -2,7 +2,6 @@ import { Effect, FileSystem, Schema } from "effect"; import { randomUUID } from "node:crypto"; import os from "node:os"; import path from "node:path"; -import { PostHog } from "posthog-node"; import { applicationRuntime } from "../runtime"; @@ -85,55 +84,38 @@ const sanitizeProperties = (properties: TelemetryProperties) => Object.entries(properties).filter(([, value]) => value !== undefined), ) as Record>; -export const shutdownTelemetryClientEffect = ( - client: Pick, - timeoutMs = TELEMETRY_TIMEOUT_MS, -) => - Effect.tryPromise({ - try: () => Promise.resolve(client.shutdown(timeoutMs)), - catch: () => undefined, - }).pipe( - Effect.timeout(timeoutMs), - Effect.interruptible, - Effect.catch(() => Effect.void), - ); - export const trackCliTelemetryEffect = Effect.fn("Telemetry.track")(function* ( event: string, properties: TelemetryProperties, ) { if (shouldDisableTelemetry()) return; const distinctId = yield* getAnonymousIdEffect(); - const client = yield* Effect.acquireRelease( - Effect.sync( - () => - new PostHog(TELEMETRY_API_KEY, { - host: TELEMETRY_HOST, - disableGeoip: true, - flushAt: 1, - flushInterval: 0, + yield* Effect.tryPromise(async (signal) => { + const response = await fetch(new URL("/i/v0/e/", TELEMETRY_HOST), { + method: "POST", + headers: { "content-type": "application/json" }, + redirect: "error", + body: JSON.stringify({ + api_key: TELEMETRY_API_KEY, + distinct_id: distinctId, + event, + properties: sanitizeProperties({ + ...getCommonProperties(), + ...properties, + $process_person_profile: false, + $geoip_disable: true, }), - ), - (posthog) => shutdownTelemetryClientEffect(posthog), - ); - yield* Effect.tryPromise(() => - client.captureImmediate({ - distinctId, - event, - properties: sanitizeProperties({ - ...getCommonProperties(), - ...properties, - $process_person_profile: false, + timestamp: new Date().toISOString(), }), - disableGeoip: true, - }), - ); + signal, + }); + await response.body?.cancel(); + }); }); export function trackCliTelemetry(event: string, properties: TelemetryProperties): Promise { return applicationRuntime.runPromise( trackCliTelemetryEffect(event, properties).pipe( - Effect.scoped, Effect.timeout(TELEMETRY_TIMEOUT_MS), Effect.catch(() => Effect.void), ), diff --git a/src/telemetry/create.ts b/src/telemetry/create.ts index c98fc40..b21d0c6 100644 --- a/src/telemetry/create.ts +++ b/src/telemetry/create.ts @@ -145,7 +145,6 @@ export const trackCreateCompletedEffect = Effect.fn("Telemetry.createCompleted") "duration-ms": params.durationMs, }, ).pipe( - Effect.scoped, Effect.timeout(TELEMETRY_TIMEOUT_MS), Effect.catch(() => Effect.void), ); @@ -184,7 +183,6 @@ export const trackCreateFailedEffect = Effect.fn("Telemetry.createFailed")(funct : null, }, ).pipe( - Effect.scoped, Effect.timeout(TELEMETRY_TIMEOUT_MS), Effect.catch(() => Effect.void), ); @@ -207,7 +205,6 @@ export const trackCreateCancelledEffect = Effect.fn("Telemetry.createCancelled") "cancellation-stage": params.stage, }, ).pipe( - Effect.scoped, Effect.timeout(TELEMETRY_TIMEOUT_MS), Effect.catch(() => Effect.void), ); diff --git a/tests/install.test.ts b/tests/install.test.ts index fcac10a..c74f8d5 100644 --- a/tests/install.test.ts +++ b/tests/install.test.ts @@ -631,5 +631,5 @@ describe("generated templates", () => { } } } - }); + }, 30_000); }); diff --git a/tests/telemetry-client.test.ts b/tests/telemetry-client.test.ts index 8bec2d8..03130d4 100644 --- a/tests/telemetry-client.test.ts +++ b/tests/telemetry-client.test.ts @@ -1,26 +1,159 @@ import { describe, expect, test } from "bun:test"; -import { Effect } from "effect"; +import { execa } from "execa"; +import { fileURLToPath } from "node:url"; -import { shutdownTelemetryClientEffect } from "../src/telemetry/client"; +const clientPath = fileURLToPath(new URL("../src/telemetry/client.ts", import.meta.url)); +const anonymousId = "40f9096f-9b67-4de2-83ec-9d56d77f527b"; +const optOuts = [ + "DO_NOT_TRACK", + "CREATE_PRISMA_DISABLE_TELEMETRY", + "CREATE_PRISMA_TELEMETRY_DISABLED", + "CI", + "GITHUB_ACTIONS", +]; -describe("telemetry client shutdown", () => { - test("bounds cleanup even when the client does not settle", async () => { - let receivedTimeout: number | undefined; - const startedAt = performance.now(); +async function sendTelemetry( + host: string, + env: Record = {}, + events = ["cli:create_prisma_next_command_completed"], +) { + const result = await execa( + process.execPath, + [ + "--eval", + ` + import { Effect, FileSystem } from "effect"; + import { trackCliTelemetryEffect, TELEMETRY_TIMEOUT_MS } from ${JSON.stringify(clientPath)}; + const fs = FileSystem.makeNoop({ + readFileString: () => Effect.succeed(${JSON.stringify(JSON.stringify({ anonymousId }))}), + }); + for (const event of ${JSON.stringify(events)}) { + await Effect.runPromise(trackCliTelemetryEffect(event, { + "failure-stage": "composer_deploy", + "error-code": "ASSEMBLE.BUILD_FAILED", + optional: undefined, + }).pipe( + Effect.provideService(FileSystem.FileSystem, fs), + Effect.timeout(TELEMETRY_TIMEOUT_MS), + Effect.catch(() => Effect.void), + )); + } + `, + ], + { + env: { + ...Object.fromEntries(optOuts.map((key) => [key, undefined])), + CREATE_PRISMA_TELEMETRY_API_KEY: "test-project-token", + CREATE_PRISMA_TELEMETRY_HOST: host, + CREATE_PRISMA_CLI_VERSION: "test-version", + ...env, + }, + timeout: 5_000, + }, + ); + expect(result.stdout).toBe(""); + expect(result.stderr).toBe(""); +} - await Effect.runPromise( - Effect.acquireRelease( - Effect.succeed({ - shutdown: (timeoutMs) => { - receivedTimeout = timeoutMs; - return new Promise(() => {}); +describe("telemetry POST", () => { + test("sends one anonymous request per outcome with the existing properties and identity", async () => { + const requests: Array<{ + method: string; + path: string; + contentType: string | null; + body: { properties: Record; timestamp: string }; + }> = []; + const server = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + async fetch(request) { + requests.push({ + method: request.method, + path: new URL(request.url).pathname, + contentType: request.headers.get("content-type"), + body: await request.json(), + }); + return new Response(null, { status: 204 }); + }, + }); + const events = [ + "cli:create_prisma_next_command_completed", + "cli:create_prisma_next_command_failed", + "cli:create_prisma_next_command_cancelled", + ]; + try { + await sendTelemetry(server.url.toString(), {}, events); + expect(requests).toHaveLength(events.length); + for (const [index, request] of requests.entries()) { + expect(request).toMatchObject({ + method: "POST", + path: "/i/v0/e/", + contentType: "application/json", + body: { + api_key: "test-project-token", + distinct_id: anonymousId, + event: events[index], + properties: { + "cli-version": "test-version", + platform: process.platform, + arch: process.arch, + "failure-stage": "composer_deploy", + "error-code": "ASSEMBLE.BUILD_FAILED", + $process_person_profile: false, + $geoip_disable: true, + }, }, - }), - (client) => shutdownTelemetryClientEffect(client, 20), - ).pipe(Effect.scoped), - ); + }); + expect(request.body.properties).not.toHaveProperty("optional"); + expect(Number.isNaN(Date.parse(request.body.timestamp))).toBe(false); + } + } finally { + server.stop(true); + } + }); + + test("does not send when opted out, in CI, or without a project token", async () => { + let requests = 0; + const server = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + fetch() { + requests++; + return new Response(null, { status: 204 }); + }, + }); + try { + for (const key of optOuts) await sendTelemetry(server.url.toString(), { [key]: "1" }); + await sendTelemetry(server.url.toString(), { CREATE_PRISMA_TELEMETRY_API_KEY: "" }); + expect(requests).toBe(0); + } finally { + server.stop(true); + } + }); - expect(receivedTimeout).toBe(20); - expect(performance.now() - startedAt).toBeLessThan(500); + test("aborts a stalled request and never retries or prints telemetry failures", async () => { + let requests = 0; + let aborted = false; + const server = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + fetch(request) { + requests++; + request.signal.addEventListener("abort", () => { + aborted = true; + }); + return new Promise(() => {}); + }, + }); + try { + const started = performance.now(); + await sendTelemetry(server.url.toString()); + expect(performance.now() - started).toBeLessThan(3_500); + expect(requests).toBe(1); + expect(aborted).toBe(true); + } finally { + server.stop(true); + } + await sendTelemetry("http://127.0.0.1:1"); }); }); From 17e7049dd8f8ca75bd03cfca33a9e6faa626fe7a Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Thu, 8 Oct 2026 13:12:40 +0530 Subject: [PATCH 2/5] test: run telemetry transport assertions on Node Signed-off-by: Aman Varshney --- tests/telemetry-client.test.ts | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/tests/telemetry-client.test.ts b/tests/telemetry-client.test.ts index 03130d4..e9db9ae 100644 --- a/tests/telemetry-client.test.ts +++ b/tests/telemetry-client.test.ts @@ -1,8 +1,26 @@ -import { describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { execa } from "execa"; -import { fileURLToPath } from "node:url"; +import { mkdtemp, rm } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; -const clientPath = fileURLToPath(new URL("../src/telemetry/client.ts", import.meta.url)); +let bundleDir: string; +let clientPath: string; +beforeAll(async () => { + bundleDir = await mkdtemp(path.join(import.meta.dirname, ".telemetry-client-")); + clientPath = path.join(bundleDir, "client.mjs"); + await execa(process.execPath, [ + "build", + fileURLToPath(new URL("../src/telemetry/client.ts", import.meta.url)), + "--target=node", + "--packages=external", + "--outfile", + clientPath, + ]); +}); +afterAll(async () => { + if (bundleDir) await rm(bundleDir, { recursive: true, force: true }); +}); const anonymousId = "40f9096f-9b67-4de2-83ec-9d56d77f527b"; const optOuts = [ "DO_NOT_TRACK", @@ -18,12 +36,13 @@ async function sendTelemetry( events = ["cli:create_prisma_next_command_completed"], ) { const result = await execa( - process.execPath, + "node", [ + "--input-type=module", "--eval", ` import { Effect, FileSystem } from "effect"; - import { trackCliTelemetryEffect, TELEMETRY_TIMEOUT_MS } from ${JSON.stringify(clientPath)}; + import { trackCliTelemetryEffect, TELEMETRY_TIMEOUT_MS } from ${JSON.stringify(pathToFileURL(clientPath).href)}; const fs = FileSystem.makeNoop({ readFileString: () => Effect.succeed(${JSON.stringify(JSON.stringify({ anonymousId }))}), }); From b60d8962829f78e68d09c305304b16b14d52d296 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Thu, 8 Oct 2026 13:47:52 +0530 Subject: [PATCH 3/5] fix: match the Prisma CLI Node minimum Signed-off-by: Aman Varshney --- .github/workflows/publish.yml | 6 +++--- README.md | 3 ++- package.json | 2 +- src/utils/node-version.ts | 4 ++-- tests/node-version.test.ts | 9 +++++---- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 93ae736..e1ac208 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -41,7 +41,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: - node-version: "22.22.0" + node-version: "22.18.0" - name: Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 @@ -82,7 +82,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: "22.22.0" + node-version: "22.18.0" registry-url: "https://registry.npmjs.org" - name: Compute preview metadata @@ -251,7 +251,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: "22.22.0" + node-version: "22.18.0" registry-url: "https://registry.npmjs.org" - name: Extract version from commit message diff --git a/README.md b/README.md index 7c4504e..39f9afd 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,8 @@ Create a Prisma 8 app with Prisma Composer built in. -Requires Node.js 22.22 or newer. +Requires Node.js 22.18 or newer, matching the Prisma CLI. +Generated apps must also meet their selected framework's Node.js requirements. ## Quick start diff --git a/package.json b/package.json index 46aad6c..4df89ca 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ }, "engines": { "bun": ">=1.3.0", - "node": ">=22.22.0" + "node": ">=22.18.0" }, "packageManager": "bun@1.4.1" } diff --git a/src/utils/node-version.ts b/src/utils/node-version.ts index 38e6c33..9515664 100644 --- a/src/utils/node-version.ts +++ b/src/utils/node-version.ts @@ -1,4 +1,4 @@ -const MINIMUM_NODE_VERSION = [22, 22, 0] as const; +const MINIMUM_NODE_VERSION = [22, 18, 0] as const; function parseVersion(version: string): [number, number, number] { const [major = "0", minor = "0", patch = "0"] = version.replace(/^v/, "").split("."); @@ -17,7 +17,7 @@ export function supportsPrisma(nodeVersion = process.versions.node): boolean { export function getUnsupportedNodeMessage(nodeVersion = process.versions.node): string { return [ `Node.js ${nodeVersion} is unsupported by create-prisma@latest.`, - "Required: Node.js 22.22 or newer.", + "Required: Node.js 22.18 or newer.", "Update Node.js and run the command again.", ].join("\n"); } diff --git a/tests/node-version.test.ts b/tests/node-version.test.ts index a4acdd2..93a1a54 100644 --- a/tests/node-version.test.ts +++ b/tests/node-version.test.ts @@ -3,13 +3,14 @@ import { describe, expect, test } from "bun:test"; import { getUnsupportedNodeMessage, supportsPrisma } from "../src/utils/node-version"; describe("Prisma 8 Node compatibility", () => { - test("requires Node 22.22 or newer", () => { - expect(supportsPrisma("22.21.9")).toBe(false); - expect(supportsPrisma("22.22.0")).toBe(true); + test("requires Node 22.18 or newer", () => { + expect(supportsPrisma("22.17.9")).toBe(false); + expect(supportsPrisma("22.18.0")).toBe(true); + expect(supportsPrisma("22.20.0")).toBe(true); expect(supportsPrisma("24.0.0")).toBe(true); }); test("returns an actionable message", () => { - expect(getUnsupportedNodeMessage("20.19.0")).toContain("Required: Node.js 22.22 or newer."); + expect(getUnsupportedNodeMessage("20.19.0")).toContain("Required: Node.js 22.18 or newer."); }); }); From 601598976d81acdbd938e3e2fc83596294d4a934 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Thu, 8 Oct 2026 15:10:12 +0530 Subject: [PATCH 4/5] test: allow generated app engine warnings on Node 22.18 Signed-off-by: Aman Varshney --- scripts/smoke-node.mjs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts/smoke-node.mjs b/scripts/smoke-node.mjs index ccecbad..05a0171 100644 --- a/scripts/smoke-node.mjs +++ b/scripts/smoke-node.mjs @@ -9,7 +9,7 @@ const env = { CREATE_PRISMA_DISABLE_TELEMETRY: "1", PRISMA_NEXT_DISABLE_TELEMETRY: "1", NO_UPDATE_NOTIFIER: "1", - npm_config_engine_strict: "true", + npm_config_engine_strict: "false", }; try { @@ -32,7 +32,11 @@ try { "--no-audit", "--no-fund", ], - { env, stdout: "inherit", stderr: "inherit" }, + { + env: { ...env, npm_config_engine_strict: "true" }, + stdout: "inherit", + stderr: "inherit", + }, ); const created = await execa( process.execPath, @@ -59,7 +63,7 @@ try { await execa("npm", ["run", "build"], options); await execa("npm", ["exec", "--", "tsc", "--noEmit"], options); console.log( - `Packed CLI, dependency engines, scaffold, build and typecheck passed on Node ${process.versions.node}.`, + `Packed CLI engine check, scaffold, build and typecheck passed on Node ${process.versions.node}.`, ); } finally { await rm(directory, { recursive: true, force: true }); From 71fe03ca67b9daef37085d038097fd31d06889d9 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Thu, 8 Oct 2026 15:21:26 +0530 Subject: [PATCH 5/5] chore(release): 0.14.1 Signed-off-by: Aman Varshney --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4df89ca..eccb45b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "create-prisma", - "version": "0.14.0", + "version": "0.14.1", "private": false, "description": "Create Prisma 8 projects with first-party templates and great DX.", "homepage": "https://github.com/prisma/create-prisma",