Skip to content

protect PostHog API key via build-time env injection#23

Merged
PunGrumpy merged 20 commits into
mainfrom
claude/posthog-api-key-protection-HGxd4
May 26, 2026
Merged

protect PostHog API key via build-time env injection#23
PunGrumpy merged 20 commits into
mainfrom
claude/posthog-api-key-protection-HGxd4

Conversation

@PunGrumpy

@PunGrumpy PunGrumpy commented May 25, 2026

Copy link
Copy Markdown
Contributor

Replace hardcoded key with process.env injection at build time using
tsdown's define option, so the key lives only in CI secrets rather
than source code. Add POSTHOG_API_KEY secret to the release workflow
build step.

https://claude.ai/code/session_01Uo4pHtL5cM9N3tZ9VCU2k9

Summary by CodeRabbit

  • New Features

    • Centralized telemetry endpoint that accepts events and optional user info; gracefully no-ops when telemetry is not configured.
  • Chores

    • Added environment configuration and example variable for telemetry key; local env files are now ignored by VCS.
    • CLI telemetry switched to sending events to the centralized collection endpoint with a short timeout.

Review Change Stack

claude added 4 commits May 25, 2026 01:16
Replace hardcoded key with process.env injection at build time using
tsdown's define option, so the key lives only in CI secrets rather
than source code. Add POSTHOG_API_KEY secret to the release workflow
build step.

https://claude.ai/code/session_01Uo4pHtL5cM9N3tZ9VCU2k9
- Add POST /api/telemetry route in Next.js app — CLI sends events here,
  server forwards to PostHog using server-side POSTHOG_API_KEY secret
- Add t3env + zod to apps/web for validated env management; migrate
  all direct process.env access to env.ts
- Remove posthog-node from CLI, replace with a fetch call to the proxy
- Remove POSTHOG_API_KEY from CLI build pipeline — key now lives only
  in server runtime (Vercel env vars)

https://claude.ai/code/session_01Uo4pHtL5cM9N3tZ9VCU2k9
NODE_ENV and VERCEL_PROJECT_PRODUCTION_URL are covered by the Vercel
preset — no need to redeclare them manually.

https://claude.ai/code/session_01Uo4pHtL5cM9N3tZ9VCU2k9
@react-doctor

react-doctor Bot commented May 25, 2026

Copy link
Copy Markdown

No new issues

Reviewed by reactreview for commit c69eecb. Configure here.

@changeset-bot

changeset-bot Bot commented May 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c69eecb

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@vernostudio/cli Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented May 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
verno-studio Ready Ready Preview, Comment May 26, 2026 2:28am

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@PunGrumpy, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 32 minutes and 2 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 26c7ade7-fd4b-4dcb-ac79-53aea584658b

📥 Commits

Reviewing files that changed from the base of the PR and between 0075afe and c69eecb.

📒 Files selected for processing (6)
  • .changeset/protect-posthog-key.md
  • apps/web/app/api/telemetry/route.ts
  • apps/web/app/page.tsx
  • apps/web/components/changelog.tsx
  • apps/web/lib/url.ts
  • packages/cli/src/analytics.ts
📝 Walkthrough

Walkthrough

Adds a typed server env for POSTHOG_API_KEY, a Zod-validated POST /api/telemetry route that forwards events to PostHog, and migrates CLI telemetry to send events via HTTP to that endpoint.

Changes

Centralized telemetry endpoint with typed environment configuration

Layer / File(s) Summary
Environment configuration and dependencies
apps/web/env.ts, apps/web/package.json, .gitignore, apps/web/.env.example
New typed env using @t3-oss/env-nextjs validating POSTHOG_API_KEY; apps/web deps updated to include @t3-oss/env-nextjs, posthog-node, and zod; .env* ignored but .env.example allowed; example adds POSTHOG_API_KEY.
Telemetry API endpoint
apps/web/app/api/telemetry/route.ts
New POST /api/telemetry handler with Zod schema (distinctId, event, optional email/name, properties), returns 204 if API key missing, 400 on validation errors, and forwards identify/capture to PostHog before returning 204; errors suppressed.
CLI telemetry migration to HTTP endpoint
packages/cli/src/analytics.ts, packages/cli/package.json
CLI removes posthog-node and instead POSTs telemetry to the web endpoint with payload merged with CLI/runtime metadata, using a 2s AbortController timeout and silent error handling.

Sequence Diagram

sequenceDiagram
  participant CLI
  participant WebTelemetryRoute
  participant PostHog
  CLI->>WebTelemetryRoute: POST /api/telemetry { distinctId, event, email?, name?, properties }
  WebTelemetryRoute->>PostHog: identify(distinctId, { email, name })   -- when email present
  WebTelemetryRoute->>PostHog: capture(distinctId, event, properties)
  WebTelemetryRoute-->>CLI: 204 No Content
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped a path from CLI to server light,
A tiny bridge for events in flight,
Typed keys and schemas guard the way,
PostHog listens, but I quietly play,
A rabbit cheers the telemetry night.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is minimal and lacks required template sections including Related Issues, Checklist, Screenshots, and Additional Notes. Only a brief description of the approach is provided without issue closure or testing confirmation. Complete the PR description following the template: add Related Issues (Closes #XX), fill in the Checklist items, and include Additional Notes if relevant.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The PR title directly describes the main objective: protecting the PostHog API key via build-time environment variable injection, which is the core focus of the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/app/api/telemetry/route.ts`:
- Line 11: The telemetry schema uses z.record with a single argument which is
incompatible with Zod v4; update the call in apps/web/app/api/telemetry/route.ts
(the properties field using z.record) to supply both key and value schemas, e.g.
change properties: z.record(z.unknown()).optional().default({}) to properties:
z.record(z.string(), z.unknown()).optional().default({}) so the record key type
is explicit and TypeScript typechecking passes.

In `@packages/cli/src/analytics.ts`:
- Around line 57-72: The telemetry fetch to TELEMETRY_URL in analytics.ts
currently awaits indefinitely; wrap the POST call with an AbortController-based
timeout: create an AbortController, start a timer (e.g., 2s-5s) that calls
controller.abort(), pass controller.signal into the fetch options (alongside
body/headers/method), and clear the timer after fetch completes (in finally) so
the CLI won't hang; update the fetch call that sends distinctId, email, event,
name and properties to use this abort signal and handle the aborted/error case
appropriately.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1c48dfbb-dca6-473d-bf38-db2cb43b825d

📥 Commits

Reviewing files that changed from the base of the PR and between fc5a8f1 and 1198b95.

📒 Files selected for processing (8)
  • apps/web/app/api/telemetry/route.ts
  • apps/web/app/page.tsx
  • apps/web/components/changelog.tsx
  • apps/web/env.ts
  • apps/web/lib/url.ts
  • apps/web/package.json
  • packages/cli/package.json
  • packages/cli/src/analytics.ts
💤 Files with no reviewable changes (1)
  • packages/cli/package.json

Comment thread apps/web/app/api/telemetry/route.ts Outdated
Comment thread packages/cli/src/analytics.ts Outdated
env.ts is only responsible for server secrets that need validation.
Client/public env vars don't need a validation layer right now.

https://claude.ai/code/session_01Uo4pHtL5cM9N3tZ9VCU2k9
zod v4 requires explicit key schema: z.record(z.string(), z.unknown())

https://claude.ai/code/session_01Uo4pHtL5cM9N3tZ9VCU2k9
zod v4 removed .email() from ZodString — use standalone z.email() instead.

https://claude.ai/code/session_01Uo4pHtL5cM9N3tZ9VCU2k9
- Replace let+try/catch with safeParse+const in telemetry route
- Add 2s AbortController timeout to CLI fetch so a stalled network
  never blocks command execution

https://claude.ai/code/session_01Uo4pHtL5cM9N3tZ9VCU2k9

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/cli/src/analytics.ts (1)

58-58: ⚡ Quick win

Extract the timeout value into a named constant.

Using 2000 directly hides intent; make it a descriptive constant for maintainability.

Suggested change
 const TELEMETRY_URL = "https://verno-studio.vercel.app/api/telemetry";
+const TELEMETRY_TIMEOUT_MS = 2_000;
 ...
-    const timeoutId = setTimeout(() => controller.abort(), 2000);
+    const timeoutId = setTimeout(() => controller.abort(), TELEMETRY_TIMEOUT_MS);

As per coding guidelines, "Use meaningful variable names instead of magic numbers - extract constants with descriptive names".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/analytics.ts` at line 58, Replace the magic number 2000 used
in setTimeout with a descriptive constant: define a named constant (e.g.,
ANALYTICS_ABORT_TIMEOUT_MS or ANALYTICS_REPORT_TIMEOUT_MS) near the top of the
module and use it in the setTimeout call that triggers controller.abort() (the
line creating timeoutId). This makes the timeout intent clear and centralizes
the value for future adjustments.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/.env.example`:
- Line 1: The .env.example currently sets POSTHOG_API_KEY="" which is an invalid
present-but-empty default and will fail validation in apps/web/env.ts where
POSTHOG_API_KEY is validated with startsWith("phc_").optional(); remove the
empty assignment or comment it out so the key is unset by default (e.g., change
POSTHOG_API_KEY="" to either omit the line or prefix with #) so teams copying
.env.example won't trigger the startsWith("phc_").optional() validation error.

In `@apps/web/app/api/telemetry/route.ts`:
- Around line 21-24: The handler currently calls await request.json() directly
which can throw on malformed/empty JSON and bypass the Zod check; wrap the
request.json() call in a try/catch, return NextResponse.json({ error: "Invalid
JSON" }, { status: 400 }) (or similar) when parsing throws, and only then call
bodySchema.safeParse(result) to validate the parsed body; update the block
around request.json(), bodySchema, and the existing NextResponse.json error
branch accordingly.
- Line 18: The handler returns JSON with status 204 (invalid) and calls await
request.json() without guarding against parse errors; update the error responses
so any response that includes a body uses a non-204 status (e.g., 400/401/500)
instead of 204 and if you must send a 204 return an empty NextResponse with no
body, and wrap the await request.json() call in a try/catch so malformed JSON
returns a NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
specifically, update the POST handler (exported default/POST function) to: 1)
check POSTHOG_API_KEY and return a JSON error with a 500/401 status (not 204) or
return NextResponse(null, { status: 204 }) if you intentionally want no body; 2)
wrap request.json() in try/catch before calling bodySchema.safeParse to catch
parse errors and return a 400 JSON error; and 3) do the same replacement for the
second occurrence where NextResponse.json(..., { status: 204 }) is used so no
JSON is ever sent with a 204 status.

---

Nitpick comments:
In `@packages/cli/src/analytics.ts`:
- Line 58: Replace the magic number 2000 used in setTimeout with a descriptive
constant: define a named constant (e.g., ANALYTICS_ABORT_TIMEOUT_MS or
ANALYTICS_REPORT_TIMEOUT_MS) near the top of the module and use it in the
setTimeout call that triggers controller.abort() (the line creating timeoutId).
This makes the timeout intent clear and centralizes the value for future
adjustments.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 63a5cac9-f5e2-4a36-9dff-e4963a341628

📥 Commits

Reviewing files that changed from the base of the PR and between 3fe33fc and 0075afe.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • .gitignore
  • apps/web/.env.example
  • apps/web/app/api/telemetry/route.ts
  • apps/web/env.ts
  • packages/cli/src/analytics.ts
✅ Files skipped from review due to trivial changes (1)
  • .gitignore

Comment thread apps/web/.env.example
Comment thread apps/web/app/api/telemetry/route.ts Outdated
Comment thread apps/web/app/api/telemetry/route.ts
@PunGrumpy PunGrumpy merged commit 95c3d2f into main May 26, 2026
7 checks passed
@PunGrumpy PunGrumpy deleted the claude/posthog-api-key-protection-HGxd4 branch May 26, 2026 02:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants