chore(main): release 2.38.0#785
Conversation
🧙 Wizard CIRun the Wizard CI and test your changes against wizard-workbench example apps by replying with a GitHub comment using one of the following commands: Test all apps:
Test all apps in a directory:
Test an individual app:
Show more apps
Results will be posted here when complete. |
b67b152 to
cb9b769
Compare
cb9b769 to
7188a5b
Compare
|
/wizard-ci all |
🧙 Wizard CI ResultsTrigger ID:
Configuration
|
🧪 Release test — 2.38.0 on a production repo:
|
| Dimension | Verdict |
|---|---|
| File analysis | ✅ correct files & locations, clean additive code |
| PostHog implementation | identify (client and server), capture_exceptions all correct — but the SDK was never added to package.json |
| Event quality | ✅ 12 real product events (website_created, team_joined, funnel_report_saved, server_login, …), enriched props (username, role), no PII dumps |
| App sanity | ❌ won't build — imports posthog-js/posthog-node in 14 files, but neither dependency is in package.json |
Confidence: 3/5. Thorough on a real codebase — identify on both sides, a correct 3-rule /ingest reverse proxy, 12 meaningful events, a written posthog-setup-report.md. Undercut by one build-breaking miss: imports the SDKs but never declares them, so pnpm install && next build fails. A 4/5 with the deps added.
📸 Snapshots — all 28 frames
01-intro
02-auth
03-run
04-run
05-run
06-run
07-run
08-run
09-run
10-run
11-run
12-run
13-run
14-run
15-run
16-run
17-run
18-run
19-run
20-run
21-run
22-run
23-run
24-run
25-outro
26-mcp
27-slack-connect
28-keep-skills
🔧 Full diff (17 files, +122/−3)
diff --git a/.gitignore b/.gitignore
index 7ac75d6..c6713b2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -52,3 +52,4 @@ yarn-error.log*
*.env.*
*.dev.yml
+.env.local
diff --git a/instrumentation-client.ts b/instrumentation-client.ts
new file mode 100644
index 0000000..b8ebf26
--- /dev/null
+++ b/instrumentation-client.ts
@@ -0,0 +1,9 @@
+import posthog from 'posthog-js';
+
+posthog.init(process.env.NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN!, {
+ api_host: '/ingest',
+ ui_host: 'https://us.posthog.com',
+ defaults: '2026-01-30',
+ capture_exceptions: true,
+ debug: process.env.NODE_ENV === 'development',
+});
diff --git a/next.config.ts b/next.config.ts
index 922d128..0218b68 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -246,6 +246,18 @@ export default withNextIntl({
async rewrites() {
return [
...rewrites,
+ {
+ source: '/ingest/static/:path*',
+ destination: 'https://us-assets.i.posthog.com/static/:path*',
+ },
+ {
+ source: '/ingest/array/:path*',
+ destination: 'https://us-assets.i.posthog.com/array/:path*',
+ },
+ {
+ source: '/ingest/:path*',
+ destination: 'https://us.i.posthog.com/:path*',
+ },
{
source: '/telemetry.js',
destination: '/api/scripts/telemetry',
@@ -256,6 +268,7 @@ export default withNextIntl({
},
];
},
+ skipTrailingSlashRedirect: true,
async redirects() {
return [...redirects];
},
diff --git a/posthog-setup-report.md b/posthog-setup-report.md
new file mode 100644
index 0000000..1c5c41a
--- /dev/null
+++ b/posthog-setup-report.md
@@ -0,0 +1,41 @@
+# PostHog post-wizard report
+
+The wizard has completed a deep integration of PostHog analytics into Umami. The setup covers client-side initialization via `instrumentation-client.ts` (Next.js 15.3+ pattern), a shared server-side PostHog client in `src/lib/posthog-server.ts`, a reverse proxy configured in `next.config.ts` to route PostHog traffic through `/ingest`, user identification on login (both client-side and server-side), and 12 instrumented events across key user flows.
+
+| Event | Description | File |
+|---|---|---|
+| `user_logged_in` | Fired when a user successfully logs in to Umami. | `src/app/login/LoginForm.tsx` |
+| `user_logged_out` | Fired when a user logs out of Umami. | `src/app/logout/LogoutPage.tsx` |
+| `website_created` | Fired when a user successfully adds a new website to track. | `src/app/(main)/websites/WebsiteAddForm.tsx` |
+| `website_deleted` | Fired when a user permanently deletes a tracked website. | `src/app/(main)/websites/[websiteId]/settings/WebsiteDeleteForm.tsx` |
+| `website_updated` | Fired when a user saves changes to a website's settings. | `src/app/(main)/websites/[websiteId]/settings/WebsiteEditForm.tsx` |
+| `website_reset` | Fired when a user resets all analytics data for a website. | `src/app/(main)/websites/[websiteId]/settings/WebsiteResetForm.tsx` |
+| `team_created` | Fired when a user creates a new team. | `src/app/(main)/teams/TeamAddForm.tsx` |
+| `team_joined` | Fired when a user joins an existing team using an access code. | `src/app/(main)/teams/TeamJoinForm.tsx` |
+| `board_created` | Fired when a user creates a new custom dashboard board. | `src/app/(main)/boards/BoardEditForm.tsx` |
+| `password_changed` | Fired when a user successfully changes their account password. | `src/app/(main)/settings/profile/PasswordEditForm.tsx` |
+| `funnel_report_saved` | Fired when a user saves a funnel report configuration. | `src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelEditForm.tsx` |
+| `server_login` | Server-side event capturing each login with user identity on the authentication API. | `src/app/api/auth/login/route.ts` |
+
+## Next steps
+
+We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented:
+
+- [Analytics basics (wizard) dashboard](https://us.posthog.com/project/228144/dashboard/1793612)
+- [Daily logins](https://us.posthog.com/project/228144/insights/XnyFVrzP)
+- [Websites created vs deleted](https://us.posthog.com/project/228144/insights/wHP9LAcM)
+- [Team activity — created & joined](https://us.posthog.com/project/228144/insights/GPcKhHwU)
+- [Feature adoption — boards & funnel reports](https://us.posthog.com/project/228144/insights/JLqLxknE)
+- [Website churn rate (deleted / created)](https://us.posthog.com/project/228144/insights/2RqYjMyh)
+
+## Verify before merging
+
+- [ ] Run a full production build (the wizard only verified the files it touched) and fix any lint or type errors introduced by the generated code.
+- [ ] Run the test suite — call sites that were rewritten or instrumented may need updated mocks or fixtures.
+- [ ] Add `NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN` and `NEXT_PUBLIC_POSTHOG_HOST` to `.env.example` and any monorepo/bootstrap scripts so collaborators know what to set.
+- [ ] Wire source-map upload (`posthog-cli sourcemap` or your bundler's upload step) into CI so production stack traces de-minify.
+- [ ] Confirm the returning-visitor path also calls `identify` — `LoginForm.tsx` identifies on fresh login, but returning sessions that skip login (token still valid) will remain on anonymous distinct IDs until the next explicit login.
+
+### Agent skill
+
+We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog.
diff --git a/src/app/(main)/boards/BoardEditForm.tsx b/src/app/(main)/boards/BoardEditForm.tsx
index ec405ff..bba62aa 100644
--- a/src/app/(main)/boards/BoardEditForm.tsx
+++ b/src/app/(main)/boards/BoardEditForm.tsx
@@ -10,6 +10,7 @@ import {
Select,
TextField,
} from '@umami/react-zen';
+import posthog from 'posthog-js';
import { useBoardQuery, useMessages, useNavigation, useUpdateQuery } from '@/components/hooks';
import { LinkSelect } from '@/components/input/LinkSelect';
import { PixelSelect } from '@/components/input/PixelSelect';
@@ -75,6 +76,9 @@ export function BoardEditForm({
parameters: setBoardEntity(board?.parameters, data.type, data.entityId || undefined),
});
+ if (!boardId) {
+ posthog.capture('board_created', { board_id: result.id, board_name: data.name, board_type: data.type });
+ }
toast(t(messages.saved));
touch('boards');
touch(`board:${result.id}`);
diff --git a/src/app/(main)/settings/profile/PasswordEditForm.tsx b/src/app/(main)/settings/profile/PasswordEditForm.tsx
index 8a1615a..2acf070 100644
--- a/src/app/(main)/settings/profile/PasswordEditForm.tsx
+++ b/src/app/(main)/settings/profile/PasswordEditForm.tsx
@@ -6,6 +6,7 @@ import {
FormSubmitButton,
PasswordField,
} from '@umami/react-zen';
+import posthog from 'posthog-js';
import { useMessages, useUpdateQuery } from '@/components/hooks';
export function PasswordEditForm({ onSave, onClose }) {
@@ -15,6 +16,7 @@ export function PasswordEditForm({ onSave, onClose }) {
const handleSubmit = async (data: any) => {
await mutateAsync(data, {
onSuccess: async () => {
+ posthog.capture('password_changed');
onSave();
onClose();
},
diff --git a/src/app/(main)/teams/TeamAddForm.tsx b/src/app/(main)/teams/TeamAddForm.tsx
index 2599baa..9b904c1 100644
--- a/src/app/(main)/teams/TeamAddForm.tsx
+++ b/src/app/(main)/teams/TeamAddForm.tsx
@@ -6,6 +6,7 @@ import {
FormSubmitButton,
TextField,
} from '@umami/react-zen';
+import posthog from 'posthog-js';
import { useMessages, useUpdateQuery } from '@/components/hooks';
import { UserSelect } from '@/components/input/UserSelect';
@@ -23,7 +24,8 @@ export function TeamAddForm({
const handleSubmit = async (data: any) => {
await mutateAsync(data, {
- onSuccess: async () => {
+ onSuccess: async (team: any) => {
+ posthog.capture('team_created', { team_name: data.name, team_id: team?.id });
onSave?.();
onClose?.();
},
diff --git a/src/app/(main)/teams/TeamJoinForm.tsx b/src/app/(main)/teams/TeamJoinForm.tsx
index 3aa8653..a0487c4 100644
--- a/src/app/(main)/teams/TeamJoinForm.tsx
+++ b/src/app/(main)/teams/TeamJoinForm.tsx
@@ -6,6 +6,7 @@ import {
FormSubmitButton,
TextField,
} from '@umami/react-zen';
+import posthog from 'posthog-js';
import { useMessages, useUpdateQuery } from '@/components/hooks';
export function TeamJoinForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) {
@@ -14,7 +15,8 @@ export function TeamJoinForm({ onSave, onClose }: { onSave: () => void; onClose:
const handleSubmit = async (data: any) => {
await mutateAsync(data, {
- onSuccess: async () => {
+ onSuccess: async (team: any) => {
+ posthog.capture('team_joined', { team_id: team?.id, team_name: team?.name });
touch('teams:members');
onSave?.();
onClose?.();
diff --git a/src/app/(main)/websites/WebsiteAddForm.tsx b/src/app/(main)/websites/WebsiteAddForm.tsx
index 7f28651..0b23c1f 100644
--- a/src/app/(main)/websites/WebsiteAddForm.tsx
+++ b/src/app/(main)/websites/WebsiteAddForm.tsx
@@ -1,4 +1,5 @@
import { Button, Form, FormField, FormSubmitButton, Row, TextField } from '@umami/react-zen';
+import posthog from 'posthog-js';
import { useMessages, useUpdateQuery } from '@/components/hooks';
import { DOMAIN_REGEX } from '@/lib/constants';
@@ -16,7 +17,13 @@ export function WebsiteAddForm({
const handleSubmit = async (data: any) => {
await mutateAsync(data, {
- onSuccess: async () => {
+ onSuccess: async (website: any) => {
+ posthog.capture('website_created', {
+ website_name: data.name,
+ website_domain: data.domain,
+ website_id: website?.id,
+ team_id: teamId,
+ });
onSave?.();
onClose?.();
},
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelEditForm.tsx b/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelEditForm.tsx
index 99cc52b..6fc15be 100644
--- a/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelEditForm.tsx
+++ b/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelEditForm.tsx
@@ -15,6 +15,7 @@ import {
TextField,
} from '@umami/react-zen';
import { Fragment, useState } from 'react';
+import posthog from 'posthog-js';
import { useApi, useMessages, useMobile, useReportQuery, useUpdateQuery } from '@/components/hooks';
import { Plus, X } from '@/components/icons';
import { ActionSelect } from '@/components/input/ActionSelect';
@@ -173,6 +174,12 @@ export function FunnelEditForm({
{ ...data, id, name, type: 'funnel', websiteId, parameters },
{
onSuccess: async () => {
+ posthog.capture('funnel_report_saved', {
+ report_name: name,
+ website_id: websiteId,
+ is_new: !id,
+ step_count: (parameters as any)?.steps?.length,
+ });
touch('reports:funnel');
touch(`report:${id}`);
onSave?.();
diff --git a/src/app/(main)/websites/[websiteId]/settings/WebsiteDeleteForm.tsx b/src/app/(main)/websites/[websiteId]/settings/WebsiteDeleteForm.tsx
index fb450f4..6848d89 100644
--- a/src/app/(main)/websites/[websiteId]/settings/WebsiteDeleteForm.tsx
+++ b/src/app/(main)/websites/[websiteId]/settings/WebsiteDeleteForm.tsx
@@ -1,3 +1,4 @@
+import posthog from 'posthog-js';
import { TypeConfirmationForm } from '@/components/common/TypeConfirmationForm';
import { useDeleteQuery, useMessages } from '@/components/hooks';
@@ -18,6 +19,7 @@ export function WebsiteDeleteForm({
const handleConfirm = async () => {
await mutateAsync(null, {
onSuccess: async () => {
+ posthog.capture('website_deleted', { website_id: websiteId });
touch('websites');
touch(`websites:${websiteId}`);
onSave?.();
diff --git a/src/app/(main)/websites/[websiteId]/settings/WebsiteEditForm.tsx b/src/app/(main)/websites/[websiteId]/settings/WebsiteEditForm.tsx
index 63d3703..0231bb4 100644
--- a/src/app/(main)/websites/[websiteId]/settings/WebsiteEditForm.tsx
+++ b/src/app/(main)/websites/[websiteId]/settings/WebsiteEditForm.tsx
@@ -1,4 +1,5 @@
import { Form, FormButtons, FormField, FormSubmitButton, TextField } from '@umami/react-zen';
+import posthog from 'posthog-js';
import { useMessages, useUpdateQuery, useWebsite } from '@/components/hooks';
import { DOMAIN_REGEX } from '@/lib/constants';
@@ -11,6 +12,11 @@ export function WebsiteEditForm({ websiteId, onSave }: { websiteId: string; onSa
const { shareId, ...updateData } = data;
await mutateAsync(updateData, {
onSuccess: async () => {
+ posthog.capture('website_updated', {
+ website_id: websiteId,
+ website_name: updateData.name,
+ website_domain: updateData.domain,
+ });
toast(t(messages.saved));
touch('websites');
touch(`website:${website.id}`);
diff --git a/src/app/(main)/websites/[websiteId]/settings/WebsiteResetForm.tsx b/src/app/(main)/websites/[websiteId]/settings/WebsiteResetForm.tsx
index 72a308e..1c915af 100644
--- a/src/app/(main)/websites/[websiteId]/settings/WebsiteResetForm.tsx
+++ b/src/app/(main)/websites/[websiteId]/settings/WebsiteResetForm.tsx
@@ -1,3 +1,4 @@
+import posthog from 'posthog-js';
import { TypeConfirmationForm } from '@/components/common/TypeConfirmationForm';
import { useMessages, useUpdateQuery } from '@/components/hooks';
@@ -18,6 +19,7 @@ export function WebsiteResetForm({
const handleConfirm = async () => {
await mutateAsync(null, {
onSuccess: async () => {
+ posthog.capture('website_reset', { website_id: websiteId });
onSave?.();
onClose?.();
},
diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts
index 9b2ad94..03c4326 100644
--- a/src/app/api/auth/login/route.ts
+++ b/src/app/api/auth/login/route.ts
@@ -7,6 +7,7 @@ import { checkPassword } from '@/lib/password';
import redis from '@/lib/redis';
import { parseRequest } from '@/lib/request';
import { json, unauthorized } from '@/lib/response';
+import { getPostHogClient } from '@/lib/posthog-server';
import { getAllUserTeams, getUserByUsername } from '@/queries/prisma';
export async function POST(request: Request) {
@@ -44,6 +45,11 @@ export async function POST(request: Request) {
const teams = await getAllUserTeams(id);
+ const posthog = getPostHogClient();
+ posthog.identify({ distinctId: id, properties: { username, role } });
+ posthog.capture({ distinctId: id, event: 'server_login', properties: { username, role } });
+ await posthog.shutdown();
+
return json({
token,
user: { id, username, role, createdAt, isAdmin: role === ROLES.admin, teams },
diff --git a/src/app/login/LoginForm.tsx b/src/app/login/LoginForm.tsx
index 23101b9..279bb7d 100644
--- a/src/app/login/LoginForm.tsx
+++ b/src/app/login/LoginForm.tsx
@@ -10,6 +10,7 @@ import {
TextField,
} from '@umami/react-zen';
import { useRouter } from 'next/navigation';
+import posthog from 'posthog-js';
import { useMessages, useUpdateQuery } from '@/components/hooks';
import { Logo } from '@/components/svg';
import { setClientAuthToken } from '@/lib/client';
@@ -25,6 +26,8 @@ export function LoginForm() {
onSuccess: async ({ token, user }) => {
setClientAuthToken(token);
setUser(user);
+ posthog.identify(user.id, { username: user.username, role: user.role });
+ posthog.capture('user_logged_in', { username: user.username, role: user.role });
router.push('/');
},
});
diff --git a/src/app/logout/LogoutPage.tsx b/src/app/logout/LogoutPage.tsx
index 33e1615..3604b32 100644
--- a/src/app/logout/LogoutPage.tsx
+++ b/src/app/logout/LogoutPage.tsx
@@ -1,6 +1,7 @@
'use client';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
+import posthog from 'posthog-js';
import { useApi } from '@/components/hooks';
import { removeClientAuthToken } from '@/lib/client';
import { setUser } from '@/store/app';
@@ -18,6 +19,8 @@ export function LogoutPage() {
removeClientAuthToken();
setUser(null);
+ posthog.capture('user_logged_out');
+ posthog.reset();
logout();
}, [router, post]);
diff --git a/src/lib/posthog-server.ts b/src/lib/posthog-server.ts
new file mode 100644
index 0000000..d93e269
--- /dev/null
+++ b/src/lib/posthog-server.ts
@@ -0,0 +1,9 @@
+import { PostHog } from 'posthog-node';
+
+export function getPostHogClient() {
+ return new PostHog(process.env.NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN!, {
+ host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
+ flushAt: 1,
+ flushInterval: 0,
+ });
+}



























🤖 I have created a release beep boop
2.38.0 (2026-07-03)
Features
Bug Fixes
This PR was generated with Release Please. See documentation.