diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d96edbe --- /dev/null +++ b/.env.example @@ -0,0 +1,18 @@ +# API Keys +OPENAI_API_KEY= +ANTHROPIC_API_KEY= + +# PostHog Analytics +# Used for tracking usage events in the app +VITE_POSTHOG_API_KEY=your_posthog_api_key +VITE_POSTHOG_API_HOST=https://your-posthog-instance.com +VITE_POSTHOG_UI_HOST=https://us.i.posthog.com + +# Apple Code Signing (for macOS builds) +APPLE_CODESIGN_IDENTITY= +APPLE_ID= +APPLE_APP_SPECIFIC_PASSWORD= +APPLE_TEAM_ID= +APPLE_CODESIGN_CERT_BASE64= +APPLE_CODESIGN_CERT_PASSWORD= +APPLE_CODESIGN_KEYCHAIN_PASSWORD= diff --git a/package.json b/package.json index d9907dd..4d62ada 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,8 @@ "idb-keyval": "^6.2.2", "node-addon-api": "^8.5.0", "node-pty": "1.1.0-beta37", + "posthog-js": "^1.283.0", + "posthog-node": "^4.18.0", "radix-themes-tw": "0.2.3", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1d2d21b..e0c97f2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,6 +83,12 @@ importers: node-pty: specifier: 1.1.0-beta37 version: 1.1.0-beta37 + posthog-js: + specifier: ^1.283.0 + version: 1.284.0 + posthog-node: + specifier: ^4.18.0 + version: 4.18.0 radix-themes-tw: specifier: 0.2.3 version: 0.2.3 @@ -1220,6 +1226,9 @@ packages: resolution: {integrity: sha512-JZujBJI0EFijL0OetcZ919tYqlOuFVOQI9PeNWwO5b9jrh3LudGWmQOzg6NBIQIDAN0L4b+Yec5tnY0I0ISVBA==} engines: {node: '>=20.0.0'} + '@posthog/core@1.5.0': + resolution: {integrity: sha512-oxfV20QMNwH30jKybUyqi3yGuMghULQz1zkJgQG3rjpHDxhD2vDN6E7UpmaqgphMIvGG3Q+DgfU10zfSPA7w7w==} + '@radix-ui/colors@3.0.0': resolution: {integrity: sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==} @@ -2569,6 +2578,9 @@ packages: async@1.5.2: resolution: {integrity: sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + at-least-node@1.0.0: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} engines: {node: '>= 4.0.0'} @@ -2584,6 +2596,9 @@ packages: peerDependencies: postcss: ^8.1.0 + axios@1.13.1: + resolution: {integrity: sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==} + bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} @@ -2663,6 +2678,10 @@ packages: resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + call-me-maybe@1.0.2: resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} @@ -2780,6 +2799,10 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} @@ -2816,6 +2839,9 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + core-js@3.46.0: + resolution: {integrity: sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==} + crelt@1.0.6: resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} @@ -2884,6 +2910,10 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + deprecation@2.3.1: resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} @@ -2923,6 +2953,10 @@ packages: ds-store@0.1.6: resolution: {integrity: sha512-kY21M6Lz+76OS3bnCzjdsJSF7LBpLYGCVfavW8TgQD2XkcqIZ86W0y9qUDZu6fp7SIZzqosMDW2zi7zVFfv4hw==} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -2990,6 +3024,14 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + es6-error@4.1.1: resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} @@ -3087,6 +3129,9 @@ packages: fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + fflate@0.4.8: + resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} + filename-reserved-regex@2.0.0: resolution: {integrity: sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==} engines: {node: '>=4'} @@ -3114,10 +3159,23 @@ packages: fmix@0.1.0: resolution: {integrity: sha512-Y6hyofImk9JdzU8k5INtTXX1cu8LDlePWDFU5sftm9H+zKCr5SGrVjdhkvsim646cw5zD0nADj8oHyXMZmCZ9w==} + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + engines: {node: '>= 6'} + formatly@0.3.0: resolution: {integrity: sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==} engines: {node: '>=18.3.0'} @@ -3187,6 +3245,10 @@ packages: resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} engines: {node: '>=18'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + get-nonce@1.0.1: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} @@ -3195,6 +3257,10 @@ packages: resolution: {integrity: sha512-SCbprXGAPdIhKAXiG+Mk6yeoFH61JlYunqdFQFHDtLjJlDjFf6x07dsS8acO+xWt52jpdVo49AlVDnUVK1sDNw==} engines: {node: '>= 4.0'} + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-stream@4.1.0: resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==} engines: {node: '>=6'} @@ -3267,6 +3333,14 @@ packages: has-property-descriptors@1.0.2: resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -3631,6 +3705,10 @@ packages: resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} engines: {node: '>=10'} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + mdast-util-from-markdown@2.0.2: resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} @@ -4150,11 +4228,21 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + posthog-js@1.284.0: + resolution: {integrity: sha512-GmycRGKWdTO6gUSMn8qzzoVTryQhxVwjK2y1Mn0eV7kldLS+tZhr/wM+Z8fXBkbrRwWUjofKFgB83gd1WzbJrA==} + + posthog-node@4.18.0: + resolution: {integrity: sha512-XROs1h+DNatgKh/AlIlCtDxWzwrKdYDb2mOs58n4yN8BkGN9ewqeQwG5ApS4/IzwCb7HPttUkOVulkYatd2PIw==} + engines: {node: '>=15.0.0'} + postject@1.0.0-alpha.6: resolution: {integrity: sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==} engines: {node: '>=14.0.0'} hasBin: true + preact@10.27.2: + resolution: {integrity: sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==} + prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} @@ -4246,6 +4334,9 @@ packages: prosemirror-view@1.41.3: resolution: {integrity: sha512-SqMiYMUQNNBP9kfPhLO8WXEk/fon47vc52FQsUiJzTBuyjKgEcoAwMyF04eQ4WZ2ArMn7+ReypYL60aKngbACQ==} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + pump@3.0.3: resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} @@ -4952,6 +5043,9 @@ packages: wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + web-vitals@4.2.4: + resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -6252,6 +6346,8 @@ snapshots: transitivePeerDependencies: - react + '@posthog/core@1.5.0': {} + '@radix-ui/colors@3.0.0': {} '@radix-ui/number@1.1.1': {} @@ -7679,6 +7775,8 @@ snapshots: async@1.5.2: optional: true + asynckit@0.4.0: {} + at-least-node@1.0.0: {} author-regex@1.0.0: {} @@ -7693,6 +7791,14 @@ snapshots: postcss: 8.5.6 postcss-value-parser: 4.2.0 + axios@1.13.1: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.4 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + bail@2.0.2: {} balanced-match@1.0.2: {} @@ -7795,6 +7901,11 @@ snapshots: normalize-url: 6.1.0 responselike: 2.0.1 + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + call-me-maybe@1.0.2: {} camelcase-css@2.0.1: {} @@ -7903,6 +8014,10 @@ snapshots: colorette@2.0.20: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + comma-separated-tokens@2.0.3: {} commander@11.1.0: {} @@ -7923,6 +8038,8 @@ snapshots: convert-source-map@2.0.0: {} + core-js@3.46.0: {} + crelt@1.0.6: {} cross-dirname@0.1.0: {} @@ -7985,6 +8102,8 @@ snapshots: object-keys: 1.1.1 optional: true + delayed-stream@1.0.0: {} + deprecation@2.3.1: {} dequal@2.0.3: {} @@ -8020,6 +8139,12 @@ snapshots: tn1150: 0.1.0 optional: true + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + eastasianwidth@0.2.0: {} electron-installer-dmg@5.0.1: @@ -8078,14 +8203,23 @@ snapshots: dependencies: is-arrayish: 0.2.1 - es-define-property@1.0.1: - optional: true + es-define-property@1.0.1: {} - es-errors@1.3.0: - optional: true + es-errors@1.3.0: {} es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + es6-error@4.1.1: optional: true @@ -8237,6 +8371,8 @@ snapshots: dependencies: pend: 1.2.0 + fflate@0.4.8: {} + filename-reserved-regex@2.0.0: {} filenamify@4.3.0: @@ -8270,11 +8406,21 @@ snapshots: imul: 1.0.1 optional: true + follow-redirects@1.15.11: {} + foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 signal-exit: 4.1.0 + form-data@4.0.4: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + formatly@0.3.0: dependencies: fd-package-json: 2.0.0 @@ -8349,6 +8495,19 @@ snapshots: get-east-asian-width@1.4.0: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + get-nonce@1.0.1: {} get-package-info@1.0.0: @@ -8360,6 +8519,11 @@ snapshots: transitivePeerDependencies: - supports-color + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-stream@4.1.0: dependencies: pump: 3.0.3 @@ -8432,8 +8596,7 @@ snapshots: globrex@0.1.2: {} - gopd@1.2.0: - optional: true + gopd@1.2.0: {} got@11.8.6: dependencies: @@ -8458,6 +8621,12 @@ snapshots: es-define-property: 1.0.1 optional: true + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -8854,6 +9023,8 @@ snapshots: escape-string-regexp: 4.0.0 optional: true + math-intrinsics@1.1.0: {} + mdast-util-from-markdown@2.0.2: dependencies: '@types/mdast': 4.0.4 @@ -9463,10 +9634,26 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + posthog-js@1.284.0: + dependencies: + '@posthog/core': 1.5.0 + core-js: 3.46.0 + fflate: 0.4.8 + preact: 10.27.2 + web-vitals: 4.2.4 + + posthog-node@4.18.0: + dependencies: + axios: 1.13.1 + transitivePeerDependencies: + - debug + postject@1.0.0-alpha.6: dependencies: commander: 9.5.0 + preact@10.27.2: {} + prettier@2.8.8: {} prettier@3.5.3: {} @@ -9587,6 +9774,8 @@ snapshots: prosemirror-state: 1.4.3 prosemirror-transform: 1.10.4 + proxy-from-env@1.1.0: {} + pump@3.0.3: dependencies: end-of-stream: 1.4.5 @@ -10381,6 +10570,8 @@ snapshots: dependencies: defaults: 1.0.4 + web-vitals@4.2.4: {} + webidl-conversions@3.0.1: {} webpack-sources@3.3.3: {} diff --git a/src/main/index.ts b/src/main/index.ts index 3efc21d..ea66f02 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -10,12 +10,18 @@ import { type MenuItemConstructorOptions, shell, } from "electron"; +import { ANALYTICS_EVENTS } from "../types/analytics.js"; import { registerAgentIpc, type TaskController } from "./services/agent.js"; import { registerFsIpc } from "./services/fs.js"; import { registerGitIpc } from "./services/git.js"; import { registerOAuthHandlers } from "./services/oauth.js"; import { registerOsIpc } from "./services/os.js"; import { registerPosthogIpc } from "./services/posthog.js"; +import { + initializePostHog, + shutdownPostHog, + trackAppEvent, +} from "./services/posthog-analytics.js"; import { registerRecallIPCHandlers, setMainWindow, @@ -172,11 +178,19 @@ function createWindow(): void { }); } -app.whenReady().then(createWindow); -app.whenReady().then(ensureClaudeConfigDir); +app.whenReady().then(() => { + createWindow(); + ensureClaudeConfigDir(); + + // Initialize PostHog analytics + initializePostHog(); + trackAppEvent(ANALYTICS_EVENTS.APP_STARTED); +}); -app.on("window-all-closed", () => { +app.on("window-all-closed", async () => { if (process.platform !== "darwin") { + trackAppEvent(ANALYTICS_EVENTS.APP_QUIT); + await shutdownPostHog(); app.quit(); } }); diff --git a/src/main/services/posthog-analytics.ts b/src/main/services/posthog-analytics.ts new file mode 100644 index 0000000..ae10e48 --- /dev/null +++ b/src/main/services/posthog-analytics.ts @@ -0,0 +1,67 @@ +import { PostHog } from "posthog-node"; + +let posthogClient: PostHog | null = null; + +export function initializePostHog() { + if (posthogClient) { + return posthogClient; + } + + const apiKey = process.env.VITE_POSTHOG_API_KEY; + const apiHost = process.env.VITE_POSTHOG_API_HOST; + + if (!apiKey) { + return null; + } + + posthogClient = new PostHog(apiKey, { + host: apiHost || "https://internal-c.posthog.com", + }); + + return posthogClient; +} + +export function trackAppEvent( + eventName: string, + properties?: Record, +) { + if (!posthogClient) { + return; + } + + properties = { + ...properties, + $process_person_profile: false, + }; + + posthogClient.capture({ + distinctId: "app-event", + event: eventName, + properties, + }); +} + +export function identifyUser( + userId: string, + properties?: Record, +) { + if (!posthogClient) { + return; + } + + posthogClient.identify({ + distinctId: userId, + properties, + }); +} + +export async function shutdownPostHog() { + if (posthogClient) { + await posthogClient.shutdown(); + posthogClient = null; + } +} + +export function getPostHogClient() { + return posthogClient; +} diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index b2e1150..8468887 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -2,6 +2,7 @@ import { MainLayout } from "@components/MainLayout"; import { AuthScreen } from "@features/auth/components/AuthScreen"; import { useAuthStore } from "@features/auth/stores/authStore"; import { Flex, Spinner, Text } from "@radix-ui/themes"; +import { initializePostHog } from "@renderer/lib/analytics"; import { useEffect, useState } from "react"; import { useRecordingQuerySync } from "@/renderer/hooks/useRecordingQuerySync"; import { @@ -15,6 +16,11 @@ function App() { useRecordingQuerySync(); + // Initialize PostHog analytics + useEffect(() => { + initializePostHog(); + }, []); + useEffect(() => { initializeOAuth().finally(() => setIsLoading(false)); }, [initializeOAuth]); diff --git a/src/renderer/components/MainLayout.tsx b/src/renderer/components/MainLayout.tsx index 884d941..4c1a360 100644 --- a/src/renderer/components/MainLayout.tsx +++ b/src/renderer/components/MainLayout.tsx @@ -9,6 +9,7 @@ import { TaskDetail } from "@features/tasks/components/TaskDetail"; import { TaskList } from "@features/tasks/components/TaskList"; import { useIntegrations } from "@hooks/useIntegrations"; import { Box, Flex } from "@radix-ui/themes"; +import { track } from "@renderer/lib/analytics"; import type { Task } from "@shared/types"; import { useLayoutStore } from "@stores/layoutStore"; import { useTabStore } from "@stores/tabStore"; @@ -16,6 +17,7 @@ import { useCallback, useEffect, useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; import { Toaster } from "sonner"; import { NotetakerView } from "@/renderer/features/notetaker/components/NotetakerView"; +import { ANALYTICS_EVENTS } from "@/types/analytics"; export function MainLayout() { const { activeTabId, tabs, createTab, setActiveTab, closeTab } = @@ -95,6 +97,12 @@ export function MainLayout() { title: task.title, data: task, }); + + // Track task view + track(ANALYTICS_EVENTS.TASK_VIEWED, { + task_id: task.id, + has_repository: !!task.repository_config, + }); } }; diff --git a/src/renderer/features/auth/stores/authStore.ts b/src/renderer/features/auth/stores/authStore.ts index be54e91..bca60ba 100644 --- a/src/renderer/features/auth/stores/authStore.ts +++ b/src/renderer/features/auth/stores/authStore.ts @@ -1,4 +1,5 @@ import { PostHogAPIClient } from "@api/posthogClient"; +import { identifyUser, resetUser, track } from "@renderer/lib/analytics"; import { queryClient } from "@renderer/lib/queryClient"; import { useTabStore } from "@renderer/stores/tabStore"; import type { CloudRegion } from "@shared/types/oauth"; @@ -8,6 +9,7 @@ import { getCloudUrlFromRegion, TOKEN_REFRESH_BUFFER_MS, } from "@/constants/oauth"; +import { ANALYTICS_EVENTS } from "@/types/analytics"; const RECALL_API_URL = "https://us-west-2.recall.ai"; @@ -131,7 +133,7 @@ export const useAuthStore = create()( ); try { - await client.getCurrentUser(); + const user = await client.getCurrentUser(); set({ oauthAccessToken: tokenResponse.access_token, @@ -149,6 +151,16 @@ export const useAuthStore = create()( get().scheduleTokenRefresh(); + // Track user login + identifyUser(user.uuid, { + project_id: projectId.toString(), + region, + }); + track(ANALYTICS_EVENTS.USER_LOGGED_IN, { + project_id: projectId.toString(), + region, + }); + // Navigate to task list after successful authentication const taskListTab = useTabStore .getState() @@ -315,7 +327,7 @@ export const useAuthStore = create()( ); try { - await client.getCurrentUser(); + const user = await client.getCurrentUser(); set({ isAuthenticated: true, @@ -325,6 +337,12 @@ export const useAuthStore = create()( get().scheduleTokenRefresh(); + // Track user identity on session restoration + identifyUser(user.uuid, { + project_id: projectId.toString(), + region: tokens.cloudRegion, + }); + // Navigate to task list after successful authentication const taskListTab = useTabStore .getState() @@ -381,6 +399,10 @@ export const useAuthStore = create()( set({ defaultWorkspace: workspace }); }, logout: () => { + // Track logout before clearing state + track(ANALYTICS_EVENTS.USER_LOGGED_OUT); + resetUser(); + if (refreshTimeoutId) { clearTimeout(refreshTimeoutId); refreshTimeoutId = null; diff --git a/src/renderer/features/repository-picker/components/RepositoryPicker.tsx b/src/renderer/features/repository-picker/components/RepositoryPicker.tsx index 188532c..0db7237 100644 --- a/src/renderer/features/repository-picker/components/RepositoryPicker.tsx +++ b/src/renderer/features/repository-picker/components/RepositoryPicker.tsx @@ -3,10 +3,12 @@ import { GitBranch as GitBranchIcon } from "@phosphor-icons/react"; import { ChevronDownIcon } from "@radix-ui/react-icons"; import { Box, Button, Flex, Popover, Text, TextField } from "@radix-ui/themes"; import { useRepositoryIntegration } from "@renderer/hooks/useIntegrations"; +import { track } from "@renderer/lib/analytics"; import type { RepositoryConfig } from "@shared/types"; import { cloneStore } from "@stores/cloneStore"; import { useMemo, useRef, useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; +import { ANALYTICS_EVENTS } from "@/types/analytics"; interface RepositoryPickerProps { value: RepositoryConfig | null; @@ -90,6 +92,12 @@ export function RepositoryPicker({ onChange(repo); setSearchValue(""); setOpen(false); + + // Track repository selection + track(ANALYTICS_EVENTS.REPOSITORY_SELECTED, { + repository_provider: "github", // Currently only GitHub is supported + source: "task-creation", + }); }; const handleSearchChange = (e: React.ChangeEvent) => { diff --git a/src/renderer/features/tasks/components/CliTaskPanel.tsx b/src/renderer/features/tasks/components/CliTaskPanel.tsx index e2508cf..108f190 100644 --- a/src/renderer/features/tasks/components/CliTaskPanel.tsx +++ b/src/renderer/features/tasks/components/CliTaskPanel.tsx @@ -246,6 +246,8 @@ export function CliTaskPanel() { { description: content, repositoryConfig, + autoRun: autoRunTasks, + createdFrom: "cli", }, { onSuccess: (newTask) => { diff --git a/src/renderer/features/tasks/hooks/useTasks.ts b/src/renderer/features/tasks/hooks/useTasks.ts index 18283fc..0a3cca3 100644 --- a/src/renderer/features/tasks/hooks/useTasks.ts +++ b/src/renderer/features/tasks/hooks/useTasks.ts @@ -1,8 +1,10 @@ import { useTaskExecutionStore } from "@features/tasks/stores/taskExecutionStore"; import { useAuthenticatedMutation } from "@hooks/useAuthenticatedMutation"; import { useAuthenticatedQuery } from "@hooks/useAuthenticatedQuery"; +import { track } from "@renderer/lib/analytics"; import type { Task } from "@shared/types"; import { useQueryClient } from "@tanstack/react-query"; +import { ANALYTICS_EVENTS } from "@/types/analytics"; const taskKeys = { all: ["tasks"] as const, @@ -39,6 +41,8 @@ export function useCreateTask() { }: { description: string; repositoryConfig?: { organization: string; repository: string }; + autoRun?: boolean; + createdFrom?: "cli" | "command-menu"; }, ) => client.createTask( @@ -46,8 +50,16 @@ export function useCreateTask() { repositoryConfig, ) as unknown as Promise, { - onSuccess: () => { + onSuccess: (_task, variables) => { queryClient.invalidateQueries({ queryKey: taskKeys.lists() }); + + // Track task creation + track(ANALYTICS_EVENTS.TASK_CREATED, { + has_repository: !!variables.repositoryConfig, + auto_run: variables.autoRun || false, + created_from: variables.createdFrom || "cli", + repository_provider: variables.repositoryConfig ? "github" : "none", + }); }, }, ); diff --git a/src/renderer/features/tasks/stores/taskExecutionStore.ts b/src/renderer/features/tasks/stores/taskExecutionStore.ts index 725cb9a..5fdc107 100644 --- a/src/renderer/features/tasks/stores/taskExecutionStore.ts +++ b/src/renderer/features/tasks/stores/taskExecutionStore.ts @@ -1,6 +1,7 @@ import { useAuthStore } from "@features/auth/stores/authStore"; import { useSettingsStore } from "@features/settings/stores/settingsStore"; import type { AgentEvent } from "@posthog/agent"; +import { track } from "@renderer/lib/analytics"; import type { ClarifyingQuestion, ExecutionMode, @@ -14,6 +15,11 @@ import { expandTildePath } from "@utils/path"; import { create } from "zustand"; import { persist } from "zustand/middleware"; import { getCloudUrlFromRegion } from "@/constants/oauth"; +import type { + ExecutionMode as AnalyticsExecutionMode, + ExecutionType, +} from "@/types/analytics"; +import { ANALYTICS_EVENTS } from "@/types/analytics"; interface ArtifactEvent { type: string; @@ -368,6 +374,19 @@ export const useTaskExecutionStore = create()( const currentTaskState = store.getTaskState(taskId); + // Track task run event + const executionType: ExecutionType = currentTaskState.runMode; + const executionMode: AnalyticsExecutionMode = + currentTaskState.executionMode; + const hasRepository = !!task.repository_config; + + track(ANALYTICS_EVENTS.TASK_RUN, { + task_id: taskId, + execution_type: executionType, + execution_mode: executionMode, + has_repository: hasRepository, + }); + // Handle cloud mode - run task via API if (currentTaskState.runMode === "cloud") { const { client } = useAuthStore.getState(); diff --git a/src/renderer/lib/analytics.ts b/src/renderer/lib/analytics.ts new file mode 100644 index 0000000..8618c1f --- /dev/null +++ b/src/renderer/lib/analytics.ts @@ -0,0 +1,55 @@ +import posthog from "posthog-js/dist/module.full.no-external"; +import type { + EventPropertyMap, + UserIdentifyProperties, +} from "../../types/analytics"; + +let isInitialized = false; + +export function initializePostHog() { + const apiKey = import.meta.env.VITE_POSTHOG_API_KEY; + const apiHost = + import.meta.env.VITE_POSTHOG_API_HOST || "https://internal-c.posthog.com"; + const uiHost = + import.meta.env.VITE_POSTHOG_UI_HOST || "https://us.i.posthog.com"; + + if (!apiKey || isInitialized) { + return; + } + + posthog.init(apiKey, { + api_host: apiHost, + ui_host: uiHost, + capture_pageview: false, + capture_pageleave: false, + }); + + isInitialized = true; +} + +export function identifyUser( + userId: string, + properties?: UserIdentifyProperties, +) { + if (!isInitialized) return; + + posthog.identify(userId, properties); +} + +export function resetUser() { + if (!isInitialized) return; + + posthog.reset(); +} + +export function track( + eventName: K, + ...args: EventPropertyMap[K] extends never + ? [] + : EventPropertyMap[K] extends undefined + ? [properties?: EventPropertyMap[K]] + : [properties: EventPropertyMap[K]] +) { + if (!isInitialized) return; + posthog.capture(eventName, args[0]); +} diff --git a/src/types/analytics.ts b/src/types/analytics.ts new file mode 100644 index 0000000..98079b0 --- /dev/null +++ b/src/types/analytics.ts @@ -0,0 +1,78 @@ +// Analytics event types and properties + +export type ExecutionType = "cloud" | "local"; +export type ExecutionMode = "plan" | "execute"; +export type RepositoryProvider = "github" | "gitlab" | "local" | "none"; +export type TaskCreatedFrom = "cli" | "command-menu"; +export type RepositorySelectSource = "task-creation" | "task-detail"; + +// Event property interfaces +export interface TaskListViewProperties { + filter_type?: string; + sort_field?: string; + view_mode?: string; +} + +export interface TaskCreateProperties { + has_repository: boolean; + auto_run: boolean; + created_from: TaskCreatedFrom; + repository_provider?: RepositoryProvider; +} + +export interface TaskViewProperties { + task_id: string; + has_repository: boolean; +} + +export interface TaskRunProperties { + task_id: string; + execution_type: ExecutionType; + execution_mode: ExecutionMode; + has_repository: boolean; +} + +export interface RepositorySelectProperties { + repository_provider: RepositoryProvider; + source: RepositorySelectSource; +} + +export interface UserIdentifyProperties { + user_id?: string; + project_id?: string; + region?: string; +} + +// Event names as constants +export const ANALYTICS_EVENTS = { + // App lifecycle + APP_STARTED: "App started", + APP_QUIT: "App quit", + + // Authentication + USER_LOGGED_IN: "User logged in", + USER_LOGGED_OUT: "User logged out", + + // Task management + TASK_LIST_VIEWED: "Task list viewed", + TASK_CREATED: "Task created", + TASK_VIEWED: "Task viewed", + TASK_RUN: "Task run", + + // Repository + REPOSITORY_SELECTED: "Repository selected", +} as const; + +export type AnalyticsEvent = + (typeof ANALYTICS_EVENTS)[keyof typeof ANALYTICS_EVENTS]; + +// Event property mapping +export type EventPropertyMap = { + [ANALYTICS_EVENTS.TASK_LIST_VIEWED]: TaskListViewProperties | undefined; + [ANALYTICS_EVENTS.TASK_CREATED]: TaskCreateProperties; + [ANALYTICS_EVENTS.TASK_VIEWED]: TaskViewProperties; + [ANALYTICS_EVENTS.TASK_RUN]: TaskRunProperties; + [ANALYTICS_EVENTS.REPOSITORY_SELECTED]: RepositorySelectProperties; + [ANALYTICS_EVENTS.USER_LOGGED_IN]: UserIdentifyProperties | undefined; + [ANALYTICS_EVENTS.USER_LOGGED_OUT]: never; +}; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 7369aa8..41c1068 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -4,6 +4,11 @@ interface ImportMetaEnv { readonly DEV: boolean; readonly PROD: boolean; readonly MODE: string; + + // PostHog Analytics + readonly VITE_POSTHOG_API_KEY?: string; + readonly VITE_POSTHOG_API_HOST?: string; + readonly VITE_POSTHOG_UI_HOST?: string; } interface ImportMeta { diff --git a/~/workspace/posthog-docusaurus b/~/workspace/posthog-docusaurus new file mode 160000 index 0000000..b713712 --- /dev/null +++ b/~/workspace/posthog-docusaurus @@ -0,0 +1 @@ +Subproject commit b713712a6409fc9c3eac7e193455b73c41777123