Skip to content

Commit 2d6e8d3

Browse files
authored
Merge pull request #203 from feat/migrate-from-umami-to-posthog
feat(analytics): migrate from Umami to PostHog for SaaS product analytics
2 parents 636d1ea + c21c046 commit 2d6e8d3

28 files changed

Lines changed: 807 additions & 47 deletions

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,10 @@ VITE_SUPABASE_ANON_KEY=your-anon-key
77
# Google Identity Services — Web OAuth client ID (public)
88
VITE_GOOGLE_WEB_CLIENT_ID=your-web-client-id.apps.googleusercontent.com
99

10+
# PostHog analytics (optional — skip in local dev)
11+
VITE_POSTHOG_API_HOST=https://us.i.posthog.com
12+
VITE_POSTHOG_API_KEY=phc_your_project_token
13+
# For production with reverse proxy: VITE_POSTHOG_API_HOST=https://e.bayanflow.com
14+
1015
# Optional: Pyodide CDN override (see src/constants/pyodideCdn.js)
1116
# VITE_PYODIDE_CDN_BASE=

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ jobs:
127127
env:
128128
VITE_GIT_BRANCH: ${{ github.ref_name }}
129129
VITE_DEV_SITE_URL: 'https://dev.bayanflow.com'
130+
VITE_POSTHOG_API_KEY: ${{ secrets.VITE_POSTHOG_API_KEY }}
131+
VITE_POSTHOG_API_HOST: 'https://us.i.posthog.com'
130132

131133
- name: Upload build artifacts
132134
uses: actions/upload-artifact@v5

.github/workflows/deploy-cloudflare.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ jobs:
5151
VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL }}
5252
VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }}
5353
VITE_GOOGLE_WEB_CLIENT_ID: ${{ secrets.VITE_GOOGLE_WEB_CLIENT_ID }}
54+
VITE_POSTHOG_API_KEY: ${{ secrets.VITE_POSTHOG_API_KEY }}
55+
VITE_POSTHOG_API_HOST: ${{ github.event.workflow_run.head_branch == 'main' && 'https://e.bayanflow.com' || 'https://e.dev.bayanflow.com' }}
5456

5557
- name: Deploy to Cloudflare Workers
5658
uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # v3

.github/workflows/preview-cloudflare.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ jobs:
5858
VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL }}
5959
VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }}
6060
VITE_GOOGLE_WEB_CLIENT_ID: ${{ secrets.VITE_GOOGLE_WEB_CLIENT_ID }}
61+
VITE_POSTHOG_API_KEY: ${{ secrets.VITE_POSTHOG_API_KEY }}
62+
VITE_POSTHOG_API_HOST: https://e.dev.bayanflow.com
6163

6264
- name: Upload preview version
6365
id: deploy

SECURITY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ Bayan Flow implements several security measures:
6969

7070
- **Client-side only**: No server-side data processing or storage for algorithm execution
7171
- **Local algorithm execution**: Sorting, searching, pathfinding, and graph visualizations run entirely in the browser
72-
- **Limited third-party calls**: Umami analytics (privacy-oriented), GitHub public API (release/repo metadata), and jsDelivr (Pyodide, only when the user opens the Python panel)
72+
- **Limited third-party calls**: PostHog analytics (privacy-oriented, via first-party proxy), GitHub public API (release/repo metadata), and jsDelivr (Pyodide, only when the user opens the Python panel)
7373
- **HTTP security headers** (via Cloudflare Workers static assets `public/_headers`): Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy
7474
- **Content Security Policy**: Restricts script, connect, frame, and worker sources to approved origins
7575
- **Automated scanning**:

index.html

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -38,34 +38,6 @@
3838
})();
3939
</script>
4040

41-
<!-- Umami Analytics - Dynamically loaded based on environment -->
42-
<script>
43-
(function () {
44-
const hostname = window.location.hostname;
45-
let websiteId;
46-
if (
47-
hostname === 'dev.bayanflow.com' ||
48-
hostname.slice(-12) === '.workers.dev'
49-
) {
50-
// Develop
51-
websiteId = 'b21ffc5d-66c1-4bfc-9e54-e05dbbe26852';
52-
} else if (
53-
hostname === 'bayanflow.com' ||
54-
hostname === 'www.bayanflow.com'
55-
) {
56-
// main
57-
websiteId = 'c096589e-f026-47e9-b458-aeb6850d5d5c';
58-
}
59-
if (websiteId) {
60-
const script = document.createElement('script');
61-
script.defer = true;
62-
script.src = 'https://cloud.umami.is/script.js';
63-
script.setAttribute('data-website-id', websiteId);
64-
document.head.appendChild(script);
65-
}
66-
})();
67-
</script>
68-
6941
<!-- Open Graph / Facebook -->
7042
<meta property="og:type" content="website" />
7143
<meta property="og:url" content="https://bayanflow.com/" />

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"@monaco-editor/react": "^4.7.0",
6060
"@octokit/rest": "^22.0.1",
6161
"@phosphor-icons/react": "^2.1.10",
62+
"@posthog/react": "^1.10.3",
6263
"@remotion/media": "4.0.436",
6364
"@remotion/web-renderer": "^4.0.436",
6465
"@supabase/supabase-js": "^2.108.2",
@@ -69,6 +70,7 @@
6970
"i18next": "^25.7.1",
7071
"i18next-browser-languagedetector": "^8.2.0",
7172
"isomorphic-dompurify": "^3.18.0",
73+
"posthog-js": "^1.404.0",
7274
"react": "^19.2.0",
7375
"react-dom": "^19.2.0",
7476
"react-i18next": "^15.7.4",

pnpm-lock.yaml

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

public/_headers

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
Referrer-Policy: strict-origin-when-cross-origin
66
Permissions-Policy: camera=(), microphone=(), geolocation=(), identity-credentials-get=(self "https://accounts.google.com")
77
# Pyodide CDN origins: jsDelivr below; custom VITE_PYODIDE_CDN_BASE origins appended at build (vite.config.js)
8-
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' blob: https://cloud.umami.is https://static.cloudflareinsights.com https://cdn.jsdelivr.net https://accounts.google.com; connect-src 'self' blob: https://cloud.umami.is https://gateway.umami.is https://cloudflareinsights.com https://api.github.com https://cdn.jsdelivr.net https://www.remotion.pro https://qketsapzqpzmccljfjcm.supabase.co https://accounts.google.com https://oauth2.googleapis.com; img-src 'self' data: blob: https://api.producthunt.com https://lh3.googleusercontent.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; font-src 'self' data:; worker-src 'self' blob:; media-src 'self' blob:; frame-src https://www.youtube-nocookie.com https://accounts.google.com; object-src 'none'; base-uri 'self'; form-action 'self'
8+
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' blob: https://*.posthog.com https://static.cloudflareinsights.com https://cdn.jsdelivr.net https://accounts.google.com; connect-src 'self' blob: https://*.posthog.com https://e.bayanflow.com https://e.dev.bayanflow.com https://cloudflareinsights.com https://api.github.com https://cdn.jsdelivr.net https://www.remotion.pro https://qketsapzqpzmccljfjcm.supabase.co https://accounts.google.com https://oauth2.googleapis.com; img-src 'self' data: blob: https://api.producthunt.com https://lh3.googleusercontent.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; font-src 'self' data:; worker-src 'self' blob:; media-src 'self' blob:; frame-src https://www.youtube-nocookie.com https://accounts.google.com; object-src 'none'; base-uri 'self'; form-action 'self'
99
Link: <https://bayanflow.com/sitemap.xml>; rel="sitemap"
1010
Link: </.well-known/api-catalog>; rel="api-catalog"
1111
Link: </.well-known/agent-card.json>; rel="service-desc"

scripts/cspHeaders.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,47 @@ function getSupabaseOrigin() {
8080
}
8181
}
8282

83+
/**
84+
* Assert CSP directives required for PostHog analytics.
85+
* @param {string} csp
86+
* @param {string} source - Label for error messages (e.g. "public/_headers")
87+
* @returns {{ scriptSrc: string; connectSrc: string }}
88+
*/
89+
export function assertAnalyticsCspDirectives(csp, source) {
90+
const directives = parseCspDirectives(csp);
91+
92+
const scriptSrcTokens = (directives.get('script-src') ?? '')
93+
.split(/\s+/)
94+
.filter(Boolean);
95+
if (!scriptSrcTokens.includes('https://*.posthog.com')) {
96+
throw new Error(
97+
`${source}: script-src must include https://*.posthog.com (PostHog SDK)`
98+
);
99+
}
100+
101+
const connectSrcTokens = (directives.get('connect-src') ?? '')
102+
.split(/\s+/)
103+
.filter(Boolean);
104+
if (!connectSrcTokens.includes('https://*.posthog.com')) {
105+
throw new Error(
106+
`${source}: connect-src must include https://*.posthog.com (PostHog analytics)`
107+
);
108+
}
109+
110+
const hasProxyOrigin =
111+
connectSrcTokens.includes('https://e.bayanflow.com') ||
112+
connectSrcTokens.includes('https://e.dev.bayanflow.com');
113+
if (!hasProxyOrigin) {
114+
throw new Error(
115+
`${source}: connect-src must include https://e.bayanflow.com or https://e.dev.bayanflow.com (PostHog first-party proxy)`
116+
);
117+
}
118+
119+
const connectSrc = directives.get('connect-src') ?? '';
120+
const scriptSrc = directives.get('script-src') ?? '';
121+
return { scriptSrc, connectSrc };
122+
}
123+
83124
/**
84125
* Assert CSP directives required for Supabase auth and Google profile avatars.
85126
* @param {string} csp

0 commit comments

Comments
 (0)