diff --git a/.env.example b/.env.example index 919223108f..5edb6861be 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,7 @@ PUBLIC_APPWRITE_ENDPOINT= PUBLIC_APPWRITE_PROJECT_ID= PUBLIC_APPWRITE_PROJECT_INIT_ID= PUBLIC_GROWTH_ENDPOINT= +PUBLIC_POSTHOG_API_KEY= APPWRITE_DB_INIT_ID= APPWRITE_COL_INIT_ID= APPWRITE_API_KEY_INIT= diff --git a/Dockerfile b/Dockerfile index 47ad97cffd..0445a4c0de 100644 --- a/Dockerfile +++ b/Dockerfile @@ -45,6 +45,9 @@ ENV GITHUB_TOKEN ${GITHUB_TOKEN} ARG SENTRY_AUTH_TOKEN ENV SENTRY_AUTH_TOKEN ${SENTRY_AUTH_TOKEN} +ARG PUBLIC_POSTHOG_API_KEY +ENV PUBLIC_POSTHOG_API_KEY ${PUBLIC_POSTHOG_API_KEY} + ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" diff --git a/docker/production.yml b/docker/production.yml index b4a01beb2c..d9a669cbff 100644 --- a/docker/production.yml +++ b/docker/production.yml @@ -80,6 +80,7 @@ services: - PUBLIC_APPWRITE_COL_THREADS_ID - PUBLIC_APPWRITE_COL_MESSAGES_ID - PUBLIC_APPWRITE_FN_TLDR_ID + - PUBLIC_POSTHOG_API_KEY deploy: <<: *x-update-config mode: replicated diff --git a/docker/stage.yml b/docker/stage.yml index 56d71b6037..e4400300ef 100644 --- a/docker/stage.yml +++ b/docker/stage.yml @@ -74,6 +74,7 @@ services: - PUBLIC_APPWRITE_COL_THREADS_ID - PUBLIC_APPWRITE_COL_MESSAGES_ID - PUBLIC_APPWRITE_FN_TLDR_ID + - PUBLIC_POSTHOG_API_KEY deploy: <<: *x-update-config mode: replicated diff --git a/package.json b/package.json index 0da82b8e2e..f0aee7ab3d 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,8 @@ "oslllo-svg-fixer": "^3.0.0", "plausible-tracker": "^0.3.9", "postcss": "^8.4.49", + "posthog-js": "^1.204.0", + "posthog-node": "^4.2.1", "prettier": "^3.3.3", "prettier-plugin-svelte": "^3.2.8", "prettier-plugin-tailwindcss": "^0.6.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f9c3db6b8..c9f9f9af2e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -144,6 +144,9 @@ importers: postcss: specifier: ^8.4.49 version: 8.4.49 + posthog-node: + specifier: ^4.2.1 + version: 4.2.1 prettier: specifier: ^3.3.3 version: 3.3.3 @@ -2030,10 +2033,16 @@ packages: async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + auto-config-loader@1.7.7: resolution: {integrity: sha512-PdqcwqgJGRGU9itaMt5IAVTQODHaleUapMljNQeRt0q/qXVxru7sQa/t545XazsGn80ipZ4rp+cFUsTHP0FuBQ==} engines: {node: '>=16.0.0'} + axios@1.7.7: + resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -2199,6 +2208,10 @@ packages: resolution: {integrity: sha512-PWGsmoJFdOB0t+BeHgmtuoRZUQucOLl5ii81NBzOOGVxlgE04muFNHlR5j8i8MKbOPELBl3243AI6lGBTj5ICQ==} hasBin: true + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -2317,6 +2330,10 @@ packages: resolution: {integrity: sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==} engines: {node: '>=10'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + delegates@1.0.0: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} @@ -2621,6 +2638,10 @@ packages: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} + form-data@4.0.1: + resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} + engines: {node: '>= 6'} + fs-extra@11.2.0: resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} engines: {node: '>=14.14'} @@ -3144,6 +3165,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} @@ -3583,6 +3612,10 @@ packages: posthog-js@1.204.0: resolution: {integrity: sha512-wVt948wKPPztCZ3OeDq8y0dtaPbhbY8vFuEVBUNHOn7PohbTXr7HZ4CNhH8fXgFkx5COEzz/20wWJmEsSU5oCA==} + posthog-node@4.2.1: + resolution: {integrity: sha512-l+fsjYEkTik3m/G0pE7gMr4qBJP84LhK779oQm6MBzhBGpd4By4qieTW+4FUAlNCyzQTynn3Nhsa50c0IELSxQ==} + engines: {node: '>=15.0.0'} + preact@10.25.4: resolution: {integrity: sha512-jLdZDb+Q+odkHJ+MpW/9U5cODzqnB+fy2EiHSZES7ldV5LK7yjlVzTp7R8Xy6W6y75kfK8iWYtFVH7lvjwrCMA==} @@ -3783,6 +3816,9 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + rusha@0.8.14: + resolution: {integrity: sha512-cLgakCUf6PedEu15t8kbsjnwIFFR2D4RfL+W3iWFJ4iac7z4B0ZI8fxy4R3J956kAI68HclCFGL8MPoUVC3qVA==} + sade@1.8.1: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} @@ -6425,6 +6461,8 @@ snapshots: async@3.2.6: {} + asynckit@0.4.0: {} + auto-config-loader@1.7.7: dependencies: ini: 4.1.3 @@ -6435,6 +6473,14 @@ snapshots: toml-eslint-parser: 0.9.3 yaml-eslint-parser: 1.2.3 + axios@1.7.7: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.1 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + axobject-query@4.1.0: {} balanced-match@1.0.2: {} @@ -6647,6 +6693,10 @@ snapshots: colors-cli@1.0.33: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commander@4.1.1: {} commander@7.2.0: {} @@ -6753,6 +6803,8 @@ snapshots: rimraf: 3.0.2 slash: 3.0.0 + delayed-stream@1.0.0: {} + delegates@1.0.0: {} dequal@2.0.3: {} @@ -7084,6 +7136,12 @@ snapshots: cross-spawn: 7.0.3 signal-exit: 4.1.0 + form-data@4.0.1: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + fs-extra@11.2.0: dependencies: graceful-fs: 4.2.11 @@ -7611,6 +7669,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + mime@1.6.0: {} mime@3.0.0: {} @@ -8031,6 +8095,13 @@ snapshots: preact: 10.25.4 web-vitals: 4.2.4 + posthog-node@4.2.1: + dependencies: + axios: 1.7.7 + rusha: 0.8.14 + transitivePeerDependencies: + - debug + preact@10.25.4: {} prelude-ls@1.2.1: {} @@ -8186,6 +8257,8 @@ snapshots: dependencies: queue-microtask: 1.2.3 + rusha@0.8.14: {} + sade@1.8.1: dependencies: mri: 1.2.0 diff --git a/src/app.css b/src/app.css index f708d3d240..1507682669 100644 --- a/src/app.css +++ b/src/app.css @@ -85,7 +85,7 @@ --animate-text: fade 0.75s ease-in-out both, blur 0.75s ease-in-out both, up 0.75s ease-in-out both; --animate-scroll: scroll 60s linear infinite; - --animate-fade-in: fade 0.5s ease-in-out both; + --animate-fade-in: fade-in 0.5s ease-in-out both; --animate-marquee: marquee var(--speed, 30s) linear infinite var(--direction, forwards); /* Pink polyfills */ diff --git a/src/lib/actions/analytics.ts b/src/lib/actions/analytics.ts index ba3891d271..62b5789592 100644 --- a/src/lib/actions/analytics.ts +++ b/src/lib/actions/analytics.ts @@ -1,8 +1,8 @@ import { Analytics, type AnalyticsPlugin } from 'analytics'; import Plausible from 'plausible-tracker'; +import posthogEvent from 'posthog-js'; import { get } from 'svelte/store'; import { page } from '$app/stores'; -import posthogEvent from 'posthog-js'; import { ENV } from '$lib/system'; import { browser } from '$app/environment'; diff --git a/src/lib/experiments.ts b/src/lib/experiments.ts new file mode 100644 index 0000000000..9abe02d032 --- /dev/null +++ b/src/lib/experiments.ts @@ -0,0 +1,30 @@ +import { PUBLIC_POSTHOG_API_KEY } from '$env/static/public'; +import { PostHog } from 'posthog-node'; + +export const posthogServerClient = new PostHog(PUBLIC_POSTHOG_API_KEY, { + host: 'https://eu.i.posthog.com', + persistence: 'memory' +}); + +export const experiments = { + 'sticky-navigation_ab-test': ['control', 'sticky-nav'] +} as const; + +type Key = keyof typeof experiments; + +export const isFlagEqualTo = ( + variant: (typeof experiments)[K][number], + currentVariant?: string | boolean +) => { + return currentVariant === variant; +}; + +export const getFeatureFlag = async ( + key: Key, + variant: (typeof experiments)[K][number], + distinctId: string +) => { + const flagData = await posthogServerClient.getFeatureFlag(key, distinctId); + + return isFlagEqualTo(variant, flagData); +}; diff --git a/src/lib/layouts/Main.svelte b/src/lib/layouts/Main.svelte index ddf4251469..1f1702f6ea 100644 --- a/src/lib/layouts/Main.svelte +++ b/src/lib/layouts/Main.svelte @@ -22,30 +22,11 @@ import AnnouncementBanner from '$lib/components/AnnouncementBanner.svelte'; import InitBanner from '$lib/components/InitBanner.svelte'; import { trackEvent } from '$lib/actions/analytics'; - import MainNav, { type NavLink } from '$lib/components/MainNav.svelte'; - import posthog from 'posthog-js'; + import MainNav from '$lib/components/MainNav.svelte'; export let omitMainId = false; let theme: 'light' | 'dark' | null = 'dark'; - // posthog - let isHeaderExperiment: boolean = false; - let mounted: boolean = false; - - onMount(async () => { - if (posthog) { - if (posthog.getFeatureFlag('sticky-navigation_ab-test') === 'control') { - isHeaderExperiment = false; - } - - if (posthog.getFeatureFlag('sticky-navigation_ab-test') === 'sticky-nav') { - isHeaderExperiment = true; - } - - mounted = true; - } - }); - function setupThemeObserver() { const handleVisibility = () => { theme = getVisibleTheme(); @@ -116,38 +97,54 @@ return setupThemeObserver(); }); - let navLinks: Array = [ - { - label: 'Products', - submenu: ProductsSubmenu, - mobileSubmenu: ProductsMobileSubmenu - }, - { - label: 'Docs', - href: '/docs' - }, - { - label: 'Community', - href: '/community' - }, - { - label: 'Blog', - href: '/blog' - }, - { - label: 'Integrations', - href: '/integrations' - }, - { - label: 'Changelog', - href: '/changelog', - showBadge: hasNewChangelog?.() && !$page.url.pathname.includes('/changelog') - }, - { - label: 'Pricing', - href: '/pricing' - } - ]; + $: navLinks = $page.data.isStickyNav + ? [ + { + label: 'Products', + submenu: ProductsSubmenu, + mobileSubmenu: ProductsMobileSubmenu + }, + { + label: 'Docs', + href: '/docs' + }, + { + label: 'Pricing', + href: '/pricing' + } + ] + : [ + { + label: 'Products', + submenu: ProductsSubmenu, + mobileSubmenu: ProductsMobileSubmenu + }, + { + label: 'Docs', + href: '/docs' + }, + { + label: 'Community', + href: '/community' + }, + { + label: 'Blog', + href: '/blog' + }, + { + label: 'Integrations', + href: '/integrations' + }, + { + label: 'Changelog', + href: '/changelog', + showBadge: hasNewChangelog?.() && !$page.url.pathname.includes('/changelog') + }, + { + label: 'Pricing', + href: '/pricing' + } + ]; $: resolvedTheme = $isMobileNavOpen ? 'dark' : theme; @@ -182,7 +179,7 @@
- {#if BANNER_KEY.startsWith('init-banner-')} - - {:else} - - - We are having lots of fun on - - + {#if !$page.data.isStickyNav} + {#if BANNER_KEY.startsWith('init-banner-')} + + {:else} + + + We are having lots of fun on + + + {/if} {/if}
) => { + return crypto.createHash('sha256').update(JSON.stringify(fingerprintData)).digest('hex'); +}; + +export const load = async ({ request, getClientAddress }) => { + const clientAddress = getClientAddress(); + const headers = Object.fromEntries(request.headers); + const fingerprintData = { + ip: clientAddress, + userAgent: headers['user-agent'], + acceptLanguage: headers['accept-language'], + platform: headers['sec-ch-ua-platform'], + mobile: headers['sec-ch-ua-mobile'], + browserBrand: headers['sec-ch-ua'] + }; + + const distinctId = generateDistinctId(fingerprintData); + + const isStickyNav = await getFeatureFlag<'sticky-navigation_ab-test'>( + 'sticky-navigation_ab-test', + 'sticky-nav', + distinctId + ); + + return { + distinctId, + isStickyNav, + changelogEntries: (await getAllChangelogEntries()).length + }; +}; diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts index b521d6d96a..1123cbea4e 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -1,26 +1,15 @@ -import { getAllChangelogEntries } from './changelog/utils'; import { browser } from '$app/environment'; -import posthog from 'posthog-js'; import { PUBLIC_POSTHOG_API_KEY } from '$env/static/public'; -import fpPromise from '@fingerprintjs/fingerprintjs'; - -export const prerender = true; -export const trailingSlash = 'never'; +import posthog from 'posthog-js'; -export const load = async () => { - if (browser && PUBLIC_POSTHOG_API_KEY) { - const fp = await fpPromise.load(); - const { visitorId: distinct_id } = await fp.get(); +export const load = async ({ data }) => { + if (browser) { posthog.init(PUBLIC_POSTHOG_API_KEY, { api_host: 'https://eu.i.posthog.com', - person_profiles: 'always', persistence: 'memory' }); - - posthog.identify(distinct_id); + posthog.identify(data.distinctId); } - return { - changelogEntries: (await getAllChangelogEntries()).length - }; + return data; }; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 00087b9307..c9c77a16f9 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -309,10 +309,10 @@
    {#each infoBoxes as box} -
  • +
  • { + return data; +};