diff --git a/.github/license-clarifications.json b/.github/license-clarifications.json new file mode 100644 index 0000000000..62d387bd10 --- /dev/null +++ b/.github/license-clarifications.json @@ -0,0 +1,3 @@ +{ + "posthog-js@*": { "licenses": "Apache-2.0" } +} diff --git a/.github/workflows/ci-dist-telemetry-scan.yaml b/.github/workflows/ci-dist-telemetry-scan.yaml index 5b8a29618e..d5abb3c911 100644 --- a/.github/workflows/ci-dist-telemetry-scan.yaml +++ b/.github/workflows/ci-dist-telemetry-scan.yaml @@ -79,3 +79,22 @@ jobs: exit 1 fi echo '✅ No Mixpanel references found' + + - name: Scan dist for PostHog telemetry references + run: | + set -euo pipefail + echo '🔍 Scanning for PostHog references...' + if rg --no-ignore -n \ + -g '*.html' \ + -g '*.js' \ + -e '(?i)posthog\.init' \ + -e '(?i)posthog\.capture' \ + -e 'PostHogTelemetryProvider' \ + -e 'ph\.comfy\.org' \ + -e 'posthog-js' \ + dist; then + echo '❌ ERROR: PostHog references found in dist assets!' + echo 'PostHog must be properly tree-shaken from OSS builds.' + exit 1 + fi + echo '✅ No PostHog references found' diff --git a/.github/workflows/ci-oss-assets-validation.yaml b/.github/workflows/ci-oss-assets-validation.yaml index 8b38b229bd..a9e4c95fd5 100644 --- a/.github/workflows/ci-oss-assets-validation.yaml +++ b/.github/workflows/ci-oss-assets-validation.yaml @@ -100,6 +100,7 @@ jobs: --production \ --summary \ --excludePackages '@comfyorg/comfyui-frontend;@comfyorg/design-system;@comfyorg/registry-types;@comfyorg/shared-frontend-utils;@comfyorg/tailwind-utils;@comfyorg/comfyui-electron-types' \ + --clarificationsFile .github/license-clarifications.json \ --onlyAllow 'MIT;MIT*;Apache-2.0;BSD-2-Clause;BSD-3-Clause;ISC;0BSD;BlueOak-1.0.0;Python-2.0;CC0-1.0;Unlicense;(MIT OR Apache-2.0);(MIT OR GPL-3.0);(Apache-2.0 OR MIT);(MPL-2.0 OR Apache-2.0);CC-BY-4.0;CC-BY-3.0;GPL-3.0-only'; then echo '' echo '✅ All production dependency licenses are approved!' diff --git a/global.d.ts b/global.d.ts index 4955744e6c..d655d8c6e2 100644 --- a/global.d.ts +++ b/global.d.ts @@ -33,6 +33,8 @@ interface Window { gtm_container_id?: string ga_measurement_id?: string mixpanel_token?: string + posthog_project_token?: string + posthog_api_host?: string require_whitelist?: boolean subscription_required?: boolean max_upload_size?: number diff --git a/package.json b/package.json index 1eacb7f0ce..b53f6ce5d2 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "loglevel": "^1.9.2", "marked": "^15.0.11", "pinia": "catalog:", + "posthog-js": "catalog:", "primeicons": "catalog:", "primevue": "catalog:", "reka-ui": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf64e83106..2302a1a7bf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -261,6 +261,9 @@ catalogs: postcss-html: specifier: ^1.8.0 version: 1.8.0 + posthog-js: + specifier: ^1.358.1 + version: 1.358.1 pretty-bytes: specifier: ^7.1.0 version: 7.1.0 @@ -509,6 +512,9 @@ importers: pinia: specifier: 'catalog:' version: 3.0.4(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)) + posthog-js: + specifier: 'catalog:' + version: 1.358.1 primeicons: specifier: 'catalog:' version: 7.0.0 @@ -764,7 +770,7 @@ importers: version: 8.0.5(vite@8.0.0-beta.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3)) vitest: specifier: 'catalog:' - version: 4.0.16(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2) + version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2) vue-component-type-helpers: specifier: 'catalog:' version: 3.2.5 @@ -2436,25 +2442,21 @@ packages: resolution: {integrity: sha512-D+tPXB0tkSuDPsuXvyQIsF3f3PBWfAwIe9FkBWtVoDVYqE+jbz+tVGsjQMNWGafLE4sC8ZQdjhsxyT8I53Anbw==} cpu: [arm64] os: [linux] - libc: [glibc] '@nx/nx-linux-arm64-musl@22.5.2': resolution: {integrity: sha512-UbO527qqa8KLBi13uXto5SmxcZv1Smer7sPexJonshDlmrJsyvx5m8nm6tcSv04W5yQEL90vPlTux8dNvEDWrw==} cpu: [arm64] os: [linux] - libc: [musl] '@nx/nx-linux-x64-gnu@22.5.2': resolution: {integrity: sha512-wR6596Vr/Z+blUAmjLHG2TCQMs4O1oi9JXK1J/PoPeO9UqdHwStCJBAd61zDFSUYJe0x+dkeRQu96fE5BW8Kcg==} cpu: [x64] os: [linux] - libc: [glibc] '@nx/nx-linux-x64-musl@22.5.2': resolution: {integrity: sha512-MBXOw4AH4FWl4orwVykj/e75awTNDePogrl3pXNX9NcQLdj6JzS4e2jaALQeRBQLxQzeFvFQV/W4PBzoPV6/NA==} cpu: [x64] os: [linux] - libc: [musl] '@nx/nx-win32-arm64-msvc@22.5.2': resolution: {integrity: sha512-SaWSZkRH5uV8vP2lj6RRv+kw2IzaIDXkutReOXpooshIWZl9KjrQELNTCZTYyhLDsMlcyhSvLFlTiA4NkZ8udw==} @@ -2502,6 +2504,78 @@ packages: '@one-ini/wasm@0.1.1': resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + '@opentelemetry/api-logs@0.208.0': + resolution: {integrity: sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/core@2.2.0': + resolution: {integrity: sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.6.0': + resolution: {integrity: sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/exporter-logs-otlp-http@0.208.0': + resolution: {integrity: sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.208.0': + resolution: {integrity: sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.208.0': + resolution: {integrity: sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/resources@2.2.0': + resolution: {integrity: sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/resources@2.6.0': + resolution: {integrity: sha512-D4y/+OGe3JSuYUCBxtH5T9DSAWNcvCb/nQWIga8HNtXTVPQn59j0nTBAgaAXxUVBDl40mG3Tc76b46wPlZaiJQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.208.0': + resolution: {integrity: sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-metrics@2.2.0': + resolution: {integrity: sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.2.0': + resolution: {integrity: sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.40.0': + resolution: {integrity: sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==} + engines: {node: '>=14'} + '@oxc-project/runtime@0.112.0': resolution: {integrity: sha512-4vYtWXMnXM6EaweCxbJ6bISAhkNHeN33SihvuX3wrpqaSJA4ZEoW35i9mSvE74+GDf1yTeVE+aEHA+WBpjDk/g==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2548,49 +2622,41 @@ packages: resolution: {integrity: sha512-SVjjjtMW66Mza76PBGJLqB0KKyFTBnxmtDXLJPbL6ZPGSctcXVmujz7/WAc0rb9m2oV0cHQTtVjnq6orQnI/jg==} cpu: [arm64] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-arm64-musl@11.15.0': resolution: {integrity: sha512-JDv2/AycPF2qgzEiDeMJCcSzKNDm3KxNg0KKWipoKEMDFqfM7LxNwwSVyAOGmrYlE4l3dg290hOMsr9xG7jv9g==} cpu: [arm64] os: [linux] - libc: [musl] '@oxc-resolver/binding-linux-ppc64-gnu@11.15.0': resolution: {integrity: sha512-zbu9FhvBLW4KJxo7ElFvZWbSt4vP685Qc/Gyk/Ns3g2gR9qh2qWXouH8PWySy+Ko/qJ42+HJCLg+ZNcxikERfg==} cpu: [ppc64] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-riscv64-gnu@11.15.0': resolution: {integrity: sha512-Kfleehe6B09C2qCnyIU01xLFqFXCHI4ylzkicfX/89j+gNHh9xyNdpEvit88Kq6i5tTGdavVnM6DQfOE2qNtlg==} cpu: [riscv64] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-riscv64-musl@11.15.0': resolution: {integrity: sha512-J7LPiEt27Tpm8P+qURDwNc8q45+n+mWgyys4/V6r5A8v5gDentHRGUx3iVk5NxdKhgoGulrzQocPTZVosq25Eg==} cpu: [riscv64] os: [linux] - libc: [musl] '@oxc-resolver/binding-linux-s390x-gnu@11.15.0': resolution: {integrity: sha512-+8/d2tAScPjVJNyqa7GPGnqleTB/XW9dZJQ2D/oIM3wpH3TG+DaFEXBbk4QFJ9K9AUGBhvQvWU2mQyhK/yYn3Q==} cpu: [s390x] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-x64-gnu@11.15.0': resolution: {integrity: sha512-xtvSzH7Nr5MCZI2FKImmOdTl9kzuQ51RPyLh451tvD2qnkg3BaqI9Ox78bTk57YJhlXPuxWSOL5aZhKAc9J6qg==} cpu: [x64] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-x64-musl@11.15.0': resolution: {integrity: sha512-14YL1zuXj06+/tqsuUZuzL0T425WA/I4nSVN1kBXeC5WHxem6lQ+2HGvG+crjeJEqHgZUT62YIgj88W+8E7eyg==} cpu: [x64] os: [linux] - libc: [musl] '@oxc-resolver/binding-openharmony-arm64@11.15.0': resolution: {integrity: sha512-/7Qli+1Wk93coxnrQaU8ySlICYN8HsgyIrzqjgIkQEpI//9eUeaeIHZptNl2fMvBGeXa7k2QgLbRNaBRgpnvMw==} @@ -2664,56 +2730,48 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [glibc] '@oxfmt/binding-linux-arm64-musl@0.34.0': resolution: {integrity: sha512-H+F8+71gHQoGTFPPJ6z4dD0Fzfzi0UP8Zx94h5kUmIFThLvMq5K1Y/bUUubiXwwHfwb5C3MPjUpYijiy0rj51Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] '@oxfmt/binding-linux-ppc64-gnu@0.34.0': resolution: {integrity: sha512-dIGnzTNhCXqQD5pzBwduLg8pClm+t8R53qaE9i5h8iua1iaFAJyLffh4847CNZSlASb7gn1Ofuv7KoG/EpoGZg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@oxfmt/binding-linux-riscv64-gnu@0.34.0': resolution: {integrity: sha512-FGQ2GTTooilDte/ogwWwkHuuL3lGtcE3uKM2EcC7kOXNWdUfMY6Jx3JCodNVVbFoybv4A+HuCj8WJji2uu1Ceg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@oxfmt/binding-linux-riscv64-musl@0.34.0': resolution: {integrity: sha512-2dGbGneJ7ptOIVKMwEIHdCkdZEomh74X3ggo4hCzEXL/rl9HwfsZDR15MkqfQqAs6nVXMvtGIOMxjDYa5lwKaA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - libc: [musl] '@oxfmt/binding-linux-s390x-gnu@0.34.0': resolution: {integrity: sha512-cCtGgmrTrxq3OeSG0UAO+w6yLZTMeOF4XM9SAkNrRUxYhRQELSDQ/iNPCLyHhYNi38uHJQbS5RQweLUDpI4ajA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - libc: [glibc] '@oxfmt/binding-linux-x64-gnu@0.34.0': resolution: {integrity: sha512-7AvMzmeX+k7GdgitXp99GQoIV/QZIpAS7rwxQvC/T541yWC45nwvk4mpnU8N+V6dE5SPEObnqfhCjO80s7qIsg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] '@oxfmt/binding-linux-x64-musl@0.34.0': resolution: {integrity: sha512-uNiglhcmivJo1oDMh3hoN/Z0WsbEXOpRXZdQ3W/IkOpyV8WF308jFjSC1ZxajdcNRXWej0zgge9QXba58Owt+g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] '@oxfmt/binding-openharmony-arm64@0.34.0': resolution: {integrity: sha512-5eFsTjCyji25j6zznzlMc+wQAZJoL9oWy576xhqd2efv+N4g1swIzuSDcb1dz4gpcVC6veWe9pAwD7HnrGjLwg==} @@ -2816,56 +2874,48 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [glibc] '@oxlint/binding-linux-arm64-musl@1.49.0': resolution: {integrity: sha512-xeqkMOARgGBlEg9BQuPDf6ZW711X6BT5qjDyeM5XNowCJeTSdmMhpePJjTEiVbbr3t21sIlK8RE6X5bc04nWyQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] '@oxlint/binding-linux-ppc64-gnu@1.49.0': resolution: {integrity: sha512-uvcqRO6PnlJGbL7TeePhTK5+7/JXbxGbN+C6FVmfICDeeRomgQqrfVjf0lUrVpUU8ii8TSkIbNdft3M+oNlOsQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@oxlint/binding-linux-riscv64-gnu@1.49.0': resolution: {integrity: sha512-Dw1HkdXAwHNH+ZDserHP2RzXQmhHtpsYYI0hf8fuGAVCIVwvS6w1+InLxpPMY25P8ASRNiFN3hADtoh6lI+4lg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@oxlint/binding-linux-riscv64-musl@1.49.0': resolution: {integrity: sha512-EPlMYaA05tJ9km/0dI9K57iuMq3Tw+nHst7TNIegAJZrBPtsOtYaMFZEaWj02HA8FI5QvSnRHMt+CI+RIhXJBQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - libc: [musl] '@oxlint/binding-linux-s390x-gnu@1.49.0': resolution: {integrity: sha512-yZiQL9qEwse34aMbnMb5VqiAWfDY+fLFuoJbHOuzB1OaJZbN1MRF9Nk+W89PIpGr5DNPDipwjZb8+Q7wOywoUQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - libc: [glibc] '@oxlint/binding-linux-x64-gnu@1.49.0': resolution: {integrity: sha512-CcCDwMMXSchNkhdgvhVn3DLZ4EnBXAD8o8+gRzahg+IdSt/72y19xBgShJgadIRF0TsRcV/MhDUMwL5N/W54aQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] '@oxlint/binding-linux-x64-musl@1.49.0': resolution: {integrity: sha512-u3HfKV8BV6t6UCCbN0RRiyqcymhrnpunVmLFI8sEa5S/EBu+p/0bJ3D7LZ2KT6PsBbrB71SWq4DeFrskOVgIZg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] '@oxlint/binding-openharmony-arm64@1.49.0': resolution: {integrity: sha512-dRDpH9fw+oeUMpM4br0taYCFpW6jQtOuEIec89rOgDA1YhqwmeRcx0XYeCv7U48p57qJ1XZHeMGM9LdItIjfzA==} @@ -2929,6 +2979,12 @@ packages: '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@posthog/core@1.23.2': + resolution: {integrity: sha512-zTDdda9NuSHrnwSOfFMxX/pyXiycF4jtU1kTr8DL61dHhV+7LF6XF1ndRZZTuaGGbfbb/GJYkEsjEX9SXfNZeQ==} + + '@posthog/types@1.358.1': + resolution: {integrity: sha512-SFfhm+NHYqsk+SAxx5FlSg9FuvqsEPZidfTjPP5TYYM24fif//L+pAzxVGqaxJcnyZojIfF66NRZ3NfM5Jd+eg==} + '@primeuix/forms@0.0.2': resolution: {integrity: sha512-DpecPQd/Qf/kav4LKCaIeGuT3AkwhJzuHCkLANTVlN/zBvo8KIj3OZHsCkm0zlIMVVnaJdtx1ULNlRQdudef+A==} engines: {node: '>=12.11.0'} @@ -3028,28 +3084,24 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.0.0-rc.3': resolution: {integrity: sha512-Z03/wrqau9Bicfgb3Dbs6SYTHliELk2PM2LpG2nFd+cGupTMF5kanLEcj2vuuJLLhptNyS61rtk7SOZ+lPsTUA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] '@rolldown/binding-linux-x64-gnu@1.0.0-rc.3': resolution: {integrity: sha512-iSXXZsQp08CSilff/DCTFZHSVEpEwdicV3W8idHyrByrcsRDVh9sGC3sev6d8BygSGj3vt8GvUKBPCoyMA4tgQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-x64-musl@1.0.0-rc.3': resolution: {integrity: sha512-qaj+MFudtdCv9xZo9znFvkgoajLdc+vwf0Kz5N44g+LU5XMe+IsACgn3UG7uTRlCCvhMAGXm1XlpEA5bZBrOcw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] '@rolldown/binding-openharmony-arm64@1.0.0-rc.3': resolution: {integrity: sha512-U662UnMETyjT65gFmG9ma+XziENrs7BBnENi/27swZPYagubfHRirXHG2oMl+pEax2WvO7Kb9gHZmMakpYqBHQ==} @@ -3127,67 +3179,56 @@ packages: resolution: {integrity: sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.53.5': resolution: {integrity: sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.53.5': resolution: {integrity: sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.53.5': resolution: {integrity: sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.53.5': resolution: {integrity: sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.53.5': resolution: {integrity: sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.53.5': resolution: {integrity: sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.53.5': resolution: {integrity: sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.53.5': resolution: {integrity: sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.53.5': resolution: {integrity: sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.53.5': resolution: {integrity: sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openharmony-arm64@4.53.5': resolution: {integrity: sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==} @@ -3463,28 +3504,24 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.2.0': resolution: {integrity: sha512-XKcSStleEVnbH6W/9DHzZv1YhjE4eSS6zOu2eRtYAIh7aV4o3vIBs+t/B15xlqoxt6ef/0uiqJVB6hkHjWD/0A==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.2.0': resolution: {integrity: sha512-/hlXCBqn9K6fi7eAM0RsobHwJYa5V/xzWspVTzxnX+Ft9v6n+30Pz8+RxCn7sQL/vRHHLS30iQPrHQunu6/vJA==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.2.0': resolution: {integrity: sha512-lKUaygq4G7sWkhQbfdRRBkaq4LY39IriqBQ+Gk6l5nKq6Ay2M2ZZb1tlIyRNgZKS8cbErTwuYSor0IIULC0SHw==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.2.0': resolution: {integrity: sha512-xuDjhAsFdUuFP5W9Ze4k/o4AskUtI8bcAGU4puTYprr89QaYFmhYOPfP+d1pH+k9ets6RoE23BXZM1X1jJqoyw==} @@ -3960,49 +3997,41 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] - libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -4869,6 +4898,9 @@ packages: core-js-compat@3.48.0: resolution: {integrity: sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==} + core-js@3.48.0: + resolution: {integrity: sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -5552,6 +5584,9 @@ packages: picomatch: optional: true + fflate@0.4.8: + resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} + fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} @@ -6372,28 +6407,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.31.1: resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.31.1: resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.31.1: resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.31.1: resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} @@ -7148,6 +7179,12 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + posthog-js@1.358.1: + resolution: {integrity: sha512-teipwLZtfErKDrURiUlLMnmpjgjGlni15JxyJ7oRaSlT3sX4E/mgvNatHIbWnp+7z1zYm3Jz5BYwGqwgyesRnw==} + + preact@10.28.4: + resolution: {integrity: sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ==} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -7309,6 +7346,9 @@ packages: quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + query-selector-shadow-dom@1.0.1: + resolution: {integrity: sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -8430,6 +8470,9 @@ packages: web-vitals@4.2.4: resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} + web-vitals@5.1.0: + resolution: {integrity: sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -10624,7 +10667,7 @@ snapshots: tsconfig-paths: 4.2.0 tslib: 2.8.1 vite: 8.0.0-beta.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2) - vitest: 4.0.16(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2) + vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2) transitivePeerDependencies: - '@babel/traverse' - '@swc-node/register' @@ -10644,7 +10687,7 @@ snapshots: tslib: 2.8.1 optionalDependencies: vite: 8.0.0-beta.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2) - vitest: 4.0.16(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2) + vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2) transitivePeerDependencies: - '@babel/traverse' - '@swc-node/register' @@ -10673,6 +10716,82 @@ snapshots: '@one-ini/wasm@0.1.1': {} + '@opentelemetry/api-logs@0.208.0': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/api@1.9.0': {} + + '@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/exporter-logs-otlp-http@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.208.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-exporter-base@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-transformer@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.208.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + protobufjs: 7.5.0 + + '@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/resources@2.6.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/sdk-logs@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.208.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/semantic-conventions@1.40.0': {} + '@oxc-project/runtime@0.112.0': {} '@oxc-project/types@0.112.0': {} @@ -10904,6 +11023,12 @@ snapshots: '@polka/url@1.0.0-next.29': {} + '@posthog/core@1.23.2': + dependencies: + cross-spawn: 7.0.6 + + '@posthog/types@1.358.1': {} + '@primeuix/forms@0.0.2': dependencies: '@primeuix/utils': 0.3.2 @@ -11983,7 +12108,7 @@ snapshots: obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.16(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2) + vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -12046,7 +12171,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vitest: 4.0.16(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2) + vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2) '@vitest/utils@3.2.4': dependencies: @@ -12969,6 +13094,8 @@ snapshots: dependencies: browserslist: 4.28.1 + core-js@3.48.0: {} + core-util-is@1.0.3: {} cosmiconfig@7.1.0: @@ -13791,6 +13918,8 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fflate@0.4.8: {} + fflate@0.8.2: {} figures@3.2.0: @@ -15716,6 +15845,24 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + posthog-js@1.358.1: + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.208.0 + '@opentelemetry/exporter-logs-otlp-http': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) + '@posthog/core': 1.23.2 + '@posthog/types': 1.358.1 + core-js: 3.48.0 + dompurify: 3.3.1 + fflate: 0.4.8 + preact: 10.28.4 + query-selector-shadow-dom: 1.0.1 + web-vitals: 5.1.0 + + preact@10.28.4: {} + prelude-ls@1.2.1: {} prettier@3.7.4: @@ -15957,6 +16104,8 @@ snapshots: quansync@0.2.11: {} + query-selector-shadow-dom@1.0.1: {} + queue-microtask@1.2.3: {} raf-schd@4.0.3: {} @@ -17262,7 +17411,7 @@ snapshots: tsx: 4.19.4 yaml: 2.8.2 - vitest@4.0.16(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2): + vitest@4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2): dependencies: '@vitest/expect': 4.0.16 '@vitest/mocker': 4.0.16(vite@8.0.0-beta.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)) @@ -17285,6 +17434,7 @@ snapshots: vite: 8.0.0-beta.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: + '@opentelemetry/api': 1.9.0 '@types/node': 24.10.4 '@vitest/ui': 4.0.16(vitest@4.0.16) happy-dom: 20.0.11 @@ -17408,6 +17558,8 @@ snapshots: web-vitals@4.2.4: {} + web-vitals@5.1.0: {} + webidl-conversions@3.0.1: {} webidl-conversions@8.0.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index afbf8ae1f8..349662478f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -88,6 +88,7 @@ catalog: picocolors: ^1.1.1 pinia: ^3.0.4 postcss-html: ^1.8.0 + posthog-js: ^1.358.1 pretty-bytes: ^7.1.0 primeicons: ^7.0.0 primevue: ^4.2.5 diff --git a/src/platform/remoteConfig/types.ts b/src/platform/remoteConfig/types.ts index e1631fe4b3..63846e4523 100644 --- a/src/platform/remoteConfig/types.ts +++ b/src/platform/remoteConfig/types.ts @@ -29,6 +29,8 @@ export type RemoteConfig = { gtm_container_id?: string ga_measurement_id?: string mixpanel_token?: string + posthog_project_token?: string + posthog_api_host?: string subscription_required?: boolean server_health_alert?: ServerHealthAlert max_upload_size?: number diff --git a/src/platform/telemetry/initTelemetry.ts b/src/platform/telemetry/initTelemetry.ts index 1456078df6..cf12cc80d4 100644 --- a/src/platform/telemetry/initTelemetry.ts +++ b/src/platform/telemetry/initTelemetry.ts @@ -24,18 +24,21 @@ export async function initTelemetry(): Promise { { TelemetryRegistry }, { MixpanelTelemetryProvider }, { GtmTelemetryProvider }, - { ImpactTelemetryProvider } + { ImpactTelemetryProvider }, + { PostHogTelemetryProvider } ] = await Promise.all([ import('./TelemetryRegistry'), import('./providers/cloud/MixpanelTelemetryProvider'), import('./providers/cloud/GtmTelemetryProvider'), - import('./providers/cloud/ImpactTelemetryProvider') + import('./providers/cloud/ImpactTelemetryProvider'), + import('./providers/cloud/PostHogTelemetryProvider') ]) const registry = new TelemetryRegistry() registry.registerProvider(new MixpanelTelemetryProvider()) registry.registerProvider(new GtmTelemetryProvider()) registry.registerProvider(new ImpactTelemetryProvider()) + registry.registerProvider(new PostHogTelemetryProvider()) setTelemetryRegistry(registry) })() diff --git a/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.test.ts b/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.test.ts index 9eb4b2b52e..b92b49ff0b 100644 --- a/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.test.ts +++ b/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.test.ts @@ -76,18 +76,15 @@ vi.mock('@/platform/remoteConfig/remoteConfig', () => ({ remoteConfig: { value: null } })) -import { MixpanelTelemetryProvider } from './MixpanelTelemetryProvider' - -describe('MixpanelTelemetryProvider.getExecutionContext', () => { - let provider: MixpanelTelemetryProvider +import { getExecutionContext } from '../../utils/getExecutionContext' +describe('getExecutionContext', () => { beforeEach(() => { vi.clearAllMocks() hoisted.mockNodes.length = 0 for (const key of Object.keys(hoisted.mockNodeDefsByName)) { delete hoisted.mockNodeDefsByName[key] } - provider = new MixpanelTelemetryProvider() }) it('returns has_toolkit_nodes false when no toolkit nodes are present', () => { @@ -101,7 +98,7 @@ describe('MixpanelTelemetryProvider.getExecutionContext', () => { python_module: 'nodes' } - const context = provider.getExecutionContext() + const context = getExecutionContext() expect(context.has_toolkit_nodes).toBe(false) expect(context.toolkit_node_names).toEqual([]) @@ -119,7 +116,7 @@ describe('MixpanelTelemetryProvider.getExecutionContext', () => { python_module: 'nodes' } - const context = provider.getExecutionContext() + const context = getExecutionContext() expect(context.has_toolkit_nodes).toBe(true) expect(context.toolkit_node_names).toEqual(['Canny']) @@ -134,7 +131,7 @@ describe('MixpanelTelemetryProvider.getExecutionContext', () => { python_module: 'comfy_essentials' } - const context = provider.getExecutionContext() + const context = getExecutionContext() expect(context.has_toolkit_nodes).toBe(true) expect(context.toolkit_node_names).toEqual([blueprintType]) @@ -148,7 +145,7 @@ describe('MixpanelTelemetryProvider.getExecutionContext', () => { python_module: 'comfy_extras.nodes_canny' } - const context = provider.getExecutionContext() + const context = getExecutionContext() expect(context.toolkit_node_names).toEqual(['Canny']) expect(context.toolkit_node_count).toBe(2) @@ -162,7 +159,7 @@ describe('MixpanelTelemetryProvider.getExecutionContext', () => { api_node: true } - const context = provider.getExecutionContext() + const context = getExecutionContext() expect(context.has_api_nodes).toBe(true) expect(context.api_node_names).toEqual(['RecraftRemoveBackgroundNode']) @@ -173,7 +170,7 @@ describe('MixpanelTelemetryProvider.getExecutionContext', () => { it('uses node.type as tracking name when nodeDef is missing', () => { hoisted.mockNodes.push(mockNode('ImageCrop')) - const context = provider.getExecutionContext() + const context = getExecutionContext() expect(context.has_toolkit_nodes).toBe(true) expect(context.toolkit_node_names).toEqual(['ImageCrop']) diff --git a/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts b/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts index 2417329bf2..30043522cd 100644 --- a/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts +++ b/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts @@ -2,22 +2,14 @@ import type { OverridedMixpanel } from 'mixpanel-browser' import { watch } from 'vue' import { useCurrentUser } from '@/composables/auth/useCurrentUser' -import { - TOOLKIT_BLUEPRINT_MODULES, - TOOLKIT_NODE_NAMES -} from '@/constants/toolkitNodes' import { checkForCompletedTopup as checkTopupUtil, clearTopupTracking as clearTopupUtil, startTopupTracking as startTopupUtil } from '@/platform/telemetry/topupTracker' -import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' import type { AuditLog } from '@/services/customerEventsService' -import { useWorkflowTemplatesStore } from '@/platform/workflow/templates/repositories/workflowTemplatesStore' -import { app } from '@/scripts/app' -import { useNodeDefStore } from '@/stores/nodeDefStore' -import { NodeSourceType } from '@/types/nodeSource' -import { reduceAllNodes } from '@/utils/graphTraversalUtil' + +import { getExecutionContext } from '../../utils/getExecutionContext' import type { AuthMetadata, @@ -282,7 +274,7 @@ export class MixpanelTelemetryProvider implements TelemetryProvider { subscribe_to_run?: boolean trigger_source?: ExecutionTriggerSource }): void { - const executionContext = this.getExecutionContext() + const executionContext = getExecutionContext() const runButtonProperties: RunButtonProperties = { subscribe_to_run: options?.subscribe_to_run || false, @@ -407,7 +399,7 @@ export class MixpanelTelemetryProvider implements TelemetryProvider { } trackWorkflowExecution(): void { - const context = this.getExecutionContext() + const context = getExecutionContext() const eventContext: ExecutionContext = { ...context, trigger_source: this.lastTriggerSource ?? 'unknown' @@ -431,117 +423,4 @@ export class MixpanelTelemetryProvider implements TelemetryProvider { trackUiButtonClicked(metadata: UiButtonClickMetadata): void { this.trackEvent(TelemetryEvents.UI_BUTTON_CLICKED, metadata) } - - getExecutionContext(): ExecutionContext { - const workflowStore = useWorkflowStore() - const templatesStore = useWorkflowTemplatesStore() - const nodeDefStore = useNodeDefStore() - const activeWorkflow = workflowStore.activeWorkflow - - // Calculate node metrics in a single traversal - type NodeMetrics = { - custom_node_count: number - api_node_count: number - toolkit_node_count: number - subgraph_count: number - total_node_count: number - has_api_nodes: boolean - api_node_names: string[] - has_toolkit_nodes: boolean - toolkit_node_names: string[] - } - - const nodeCounts = reduceAllNodes( - app.rootGraph, - (metrics, node) => { - const nodeDef = nodeDefStore.nodeDefsByName[node.type] - const isCustomNode = - nodeDef?.nodeSource?.type === NodeSourceType.CustomNodes - const isApiNode = nodeDef?.api_node === true - const isSubgraph = node.isSubgraphNode?.() === true - - if (isApiNode) { - metrics.has_api_nodes = true - const canonicalName = nodeDef?.name - if ( - canonicalName && - !metrics.api_node_names.includes(canonicalName) - ) { - metrics.api_node_names.push(canonicalName) - } - } - - const isToolkitNode = - TOOLKIT_NODE_NAMES.has(node.type) || - (nodeDef?.python_module !== undefined && - TOOLKIT_BLUEPRINT_MODULES.has(nodeDef.python_module)) - if (isToolkitNode) { - metrics.has_toolkit_nodes = true - const trackingName = nodeDef?.name ?? node.type - if (!metrics.toolkit_node_names.includes(trackingName)) { - metrics.toolkit_node_names.push(trackingName) - } - } - - metrics.custom_node_count += isCustomNode ? 1 : 0 - metrics.api_node_count += isApiNode ? 1 : 0 - metrics.toolkit_node_count += isToolkitNode ? 1 : 0 - metrics.subgraph_count += isSubgraph ? 1 : 0 - metrics.total_node_count += 1 - - return metrics - }, - { - custom_node_count: 0, - api_node_count: 0, - toolkit_node_count: 0, - subgraph_count: 0, - total_node_count: 0, - has_api_nodes: false, - api_node_names: [], - has_toolkit_nodes: false, - toolkit_node_names: [] - } - ) - - if (activeWorkflow?.filename) { - const isTemplate = templatesStore.knownTemplateNames.has( - activeWorkflow.filename - ) - - if (isTemplate) { - const template = templatesStore.getTemplateByName( - activeWorkflow.filename - ) - - const englishMetadata = templatesStore.getEnglishMetadata( - activeWorkflow.filename - ) - - return { - is_template: true, - workflow_name: activeWorkflow.filename, - template_source: template?.sourceModule, - template_category: englishMetadata?.category ?? template?.category, - template_tags: englishMetadata?.tags ?? template?.tags, - template_models: englishMetadata?.models ?? template?.models, - template_use_case: englishMetadata?.useCase ?? template?.useCase, - template_license: englishMetadata?.license ?? template?.license, - ...nodeCounts - } - } - - return { - is_template: false, - workflow_name: activeWorkflow.filename, - ...nodeCounts - } - } - - return { - is_template: false, - workflow_name: undefined, - ...nodeCounts - } - } } diff --git a/src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.test.ts b/src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.test.ts new file mode 100644 index 0000000000..f36b7f5ba6 --- /dev/null +++ b/src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.test.ts @@ -0,0 +1,246 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { TelemetryEvents } from '../../types' + +const hoisted = vi.hoisted(() => { + const mockCapture = vi.fn() + const mockInit = vi.fn() + const mockIdentify = vi.fn() + const mockPeopleSet = vi.fn() + const mockOnUserResolved = vi.fn() + + return { + mockCapture, + mockInit, + mockIdentify, + mockPeopleSet, + mockOnUserResolved, + mockPosthog: { + default: { + init: mockInit, + capture: mockCapture, + identify: mockIdentify, + people: { set: mockPeopleSet } + } + } + } +}) + +vi.mock('vue', async () => { + const actual = await vi.importActual('vue') + return { + ...actual, + watch: vi.fn() + } +}) + +vi.mock('@/composables/auth/useCurrentUser', () => ({ + useCurrentUser: () => ({ + onUserResolved: hoisted.mockOnUserResolved + }) +})) + +vi.mock('@/platform/remoteConfig/remoteConfig', () => ({ + remoteConfig: { value: null } +})) + +vi.mock('posthog-js', () => hoisted.mockPosthog) + +import { PostHogTelemetryProvider } from './PostHogTelemetryProvider' + +function createProvider( + config: Partial = {} +): PostHogTelemetryProvider { + const original = window.__CONFIG__ + window.__CONFIG__ = { ...original, ...config } + const provider = new PostHogTelemetryProvider() + window.__CONFIG__ = original + return provider +} + +describe('PostHogTelemetryProvider', () => { + beforeEach(() => { + vi.clearAllMocks() + window.__CONFIG__ = { + posthog_project_token: 'phc_test_token' + } as typeof window.__CONFIG__ + }) + + describe('initialization', () => { + it('disables itself when posthog_project_token is not provided', async () => { + const provider = createProvider({ posthog_project_token: undefined }) + await vi.dynamicImportSettled() + + provider.trackSignupOpened() + + expect(hoisted.mockCapture).not.toHaveBeenCalled() + }) + + it('calls posthog.init with the token and default api_host', async () => { + createProvider() + await vi.dynamicImportSettled() + + expect(hoisted.mockInit).toHaveBeenCalledWith('phc_test_token', { + api_host: 'https://ph.comfy.org', + autocapture: false, + capture_pageview: false, + capture_pageleave: false, + persistence: 'localStorage+cookie' + }) + }) + + it('uses custom api_host from config when provided', async () => { + window.__CONFIG__ = { + posthog_project_token: 'phc_test_token', + posthog_api_host: 'https://custom.host.com' + } as typeof window.__CONFIG__ + new PostHogTelemetryProvider() + await vi.dynamicImportSettled() + + expect(hoisted.mockInit).toHaveBeenCalledWith( + 'phc_test_token', + expect.objectContaining({ api_host: 'https://custom.host.com' }) + ) + }) + + it('registers onUserResolved callback after init', async () => { + createProvider() + await vi.dynamicImportSettled() + + expect(hoisted.mockOnUserResolved).toHaveBeenCalledOnce() + }) + + it('identifies user when onUserResolved fires', async () => { + createProvider() + await vi.dynamicImportSettled() + + const callback = hoisted.mockOnUserResolved.mock.calls[0][0] + callback({ id: 'user-123' }) + + expect(hoisted.mockIdentify).toHaveBeenCalledWith('user-123') + }) + }) + + describe('event tracking', () => { + it('captures events after initialization', async () => { + const provider = createProvider() + await vi.dynamicImportSettled() + + provider.trackSignupOpened() + + expect(hoisted.mockCapture).toHaveBeenCalledWith( + TelemetryEvents.USER_SIGN_UP_OPENED, + {} + ) + }) + + it('captures events with metadata', async () => { + const provider = createProvider() + await vi.dynamicImportSettled() + + provider.trackAuth({ method: 'google' }) + + expect(hoisted.mockCapture).toHaveBeenCalledWith( + TelemetryEvents.USER_AUTH_COMPLETED, + { method: 'google' } + ) + }) + + it('queues events before initialization and flushes after', async () => { + const provider = createProvider() + + provider.trackUserLoggedIn() + expect(hoisted.mockCapture).not.toHaveBeenCalled() + + await vi.dynamicImportSettled() + + expect(hoisted.mockCapture).toHaveBeenCalledWith( + TelemetryEvents.USER_LOGGED_IN, + {} + ) + }) + }) + + describe('disabled events', () => { + it('does not capture default disabled events', async () => { + const provider = createProvider() + await vi.dynamicImportSettled() + + provider.trackWorkflowOpened({ + missing_node_count: 0, + missing_node_types: [] + }) + + expect(hoisted.mockCapture).not.toHaveBeenCalled() + }) + + it('captures events not in the disabled list', async () => { + const provider = createProvider() + await vi.dynamicImportSettled() + + provider.trackMonthlySubscriptionSucceeded() + + expect(hoisted.mockCapture).toHaveBeenCalledWith( + TelemetryEvents.MONTHLY_SUBSCRIPTION_SUCCEEDED, + {} + ) + }) + }) + + describe('survey tracking', () => { + it('sets user properties on survey submission', async () => { + const provider = createProvider() + await vi.dynamicImportSettled() + + const responses = { familiarity: 'beginner', industry: 'tech' } + provider.trackSurvey('submitted', responses) + + expect(hoisted.mockCapture).toHaveBeenCalledWith( + TelemetryEvents.USER_SURVEY_SUBMITTED, + expect.objectContaining({ familiarity: 'beginner' }) + ) + expect(hoisted.mockPeopleSet).toHaveBeenCalled() + }) + + it('does not set user properties on survey opened', async () => { + const provider = createProvider() + await vi.dynamicImportSettled() + + provider.trackSurvey('opened') + + expect(hoisted.mockCapture).toHaveBeenCalledWith( + TelemetryEvents.USER_SURVEY_OPENED, + {} + ) + expect(hoisted.mockPeopleSet).not.toHaveBeenCalled() + }) + }) + + describe('page view', () => { + it('captures page view with page_name property', async () => { + const provider = createProvider() + await vi.dynamicImportSettled() + + provider.trackPageView('workflow_editor') + + expect(hoisted.mockCapture).toHaveBeenCalledWith( + TelemetryEvents.PAGE_VIEW, + { page_name: 'workflow_editor' } + ) + }) + + it('forwards additional metadata', async () => { + const provider = createProvider() + await vi.dynamicImportSettled() + + provider.trackPageView('workflow_editor', { + path: '/workflows/123' + }) + + expect(hoisted.mockCapture).toHaveBeenCalledWith( + TelemetryEvents.PAGE_VIEW, + { page_name: 'workflow_editor', path: '/workflows/123' } + ) + }) + }) +}) diff --git a/src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.ts b/src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.ts new file mode 100644 index 0000000000..aba47173e8 --- /dev/null +++ b/src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.ts @@ -0,0 +1,417 @@ +import type { PostHog } from 'posthog-js' +import { watch } from 'vue' + +import { useCurrentUser } from '@/composables/auth/useCurrentUser' +import { remoteConfig } from '@/platform/remoteConfig/remoteConfig' +import type { RemoteConfig } from '@/platform/remoteConfig/types' + +import type { + AuthMetadata, + EnterLinearMetadata, + ExecutionContext, + ExecutionErrorMetadata, + ExecutionSuccessMetadata, + ExecutionTriggerSource, + HelpCenterClosedMetadata, + HelpCenterOpenedMetadata, + HelpResourceClickedMetadata, + NodeSearchMetadata, + NodeSearchResultMetadata, + PageViewMetadata, + PageVisibilityMetadata, + RunButtonProperties, + SettingChangedMetadata, + SubscriptionMetadata, + SurveyResponses, + TabCountMetadata, + TelemetryEventName, + TelemetryEventProperties, + TelemetryProvider, + TemplateFilterMetadata, + TemplateLibraryClosedMetadata, + TemplateLibraryMetadata, + TemplateMetadata, + UiButtonClickMetadata, + WorkflowCreatedMetadata, + WorkflowImportMetadata +} from '../../types' +import { TelemetryEvents } from '../../types' +import { getExecutionContext } from '../../utils/getExecutionContext' +import { normalizeSurveyResponses } from '../../utils/surveyNormalization' + +const DEFAULT_DISABLED_EVENTS = [ + TelemetryEvents.WORKFLOW_OPENED, + TelemetryEvents.PAGE_VISIBILITY_CHANGED, + TelemetryEvents.TAB_COUNT_TRACKING, + TelemetryEvents.NODE_SEARCH, + TelemetryEvents.NODE_SEARCH_RESULT_SELECTED, + TelemetryEvents.TEMPLATE_FILTER_CHANGED, + TelemetryEvents.SETTING_CHANGED, + TelemetryEvents.HELP_CENTER_OPENED, + TelemetryEvents.HELP_RESOURCE_CLICKED, + TelemetryEvents.HELP_CENTER_CLOSED, + TelemetryEvents.WORKFLOW_CREATED, + TelemetryEvents.UI_BUTTON_CLICKED +] as const satisfies TelemetryEventName[] + +const TELEMETRY_EVENT_SET = new Set( + Object.values(TelemetryEvents) as TelemetryEventName[] +) + +interface QueuedEvent { + eventName: TelemetryEventName + properties?: TelemetryEventProperties +} + +/** + * PostHog Telemetry Provider - Cloud Build Implementation + * + * Sends all telemetry events to PostHog so they can be correlated + * with session recordings. Follows the same pattern as MixpanelTelemetryProvider. + * + * CRITICAL: OSS Build Safety + * Entire file is tree-shaken away in OSS builds (DISTRIBUTION unset). + */ +export class PostHogTelemetryProvider implements TelemetryProvider { + private isEnabled = true + private posthog: PostHog | null = null + private eventQueue: QueuedEvent[] = [] + private isInitialized = false + private lastTriggerSource: ExecutionTriggerSource | undefined + private disabledEvents = new Set(DEFAULT_DISABLED_EVENTS) + + constructor() { + this.configureDisabledEvents( + (window.__CONFIG__ as Partial | undefined) ?? null + ) + watch( + remoteConfig, + (config) => { + this.configureDisabledEvents(config) + }, + { immediate: true } + ) + + const apiKey = window.__CONFIG__?.posthog_project_token + if (apiKey) { + try { + void import('posthog-js') + .then((posthogModule) => { + this.posthog = posthogModule.default + this.posthog!.init(apiKey, { + api_host: + window.__CONFIG__?.posthog_api_host || 'https://ph.comfy.org', + autocapture: false, + capture_pageview: false, + capture_pageleave: false, + persistence: 'localStorage+cookie' + }) + this.isInitialized = true + this.flushEventQueue() + + useCurrentUser().onUserResolved((user) => { + if (this.posthog && user.id) { + this.posthog.identify(user.id) + } + }) + }) + .catch((error) => { + console.error('Failed to load PostHog:', error) + this.isEnabled = false + }) + } catch (error) { + console.error('Failed to initialize PostHog:', error) + this.isEnabled = false + } + } else { + console.warn('PostHog API key not provided in runtime config') + this.isEnabled = false + } + } + + private flushEventQueue(): void { + if (!this.isInitialized || !this.posthog) return + + while (this.eventQueue.length > 0) { + const event = this.eventQueue.shift()! + try { + this.posthog.capture(event.eventName, event.properties || {}) + } catch (error) { + console.error('Failed to track queued PostHog event:', error) + } + } + } + + private trackEvent( + eventName: TelemetryEventName, + properties?: TelemetryEventProperties + ): void { + if (!this.isEnabled) return + if (this.disabledEvents.has(eventName)) return + + const event: QueuedEvent = { eventName, properties } + + if (this.isInitialized && this.posthog) { + try { + this.posthog.capture(eventName, properties || {}) + } catch (error) { + console.error('Failed to track PostHog event:', error) + } + } else { + this.eventQueue.push(event) + } + } + + private captureRaw( + eventName: TelemetryEventName, + properties?: Record + ): void { + if (!this.isEnabled) return + if (this.disabledEvents.has(eventName)) return + + if (this.isInitialized && this.posthog) { + try { + this.posthog.capture(eventName, properties || {}) + } catch (error) { + console.error('Failed to track PostHog event:', error) + } + } else { + this.eventQueue.push({ + eventName, + properties: properties as TelemetryEventProperties + }) + } + } + + private configureDisabledEvents(config: Partial | null): void { + const disabledSource = + config?.telemetry_disabled_events ?? DEFAULT_DISABLED_EVENTS + + this.disabledEvents = this.buildEventSet(disabledSource) + } + + private buildEventSet(values: TelemetryEventName[]): Set { + return new Set( + values.filter((value) => { + const isValid = TELEMETRY_EVENT_SET.has(value) + if (!isValid && import.meta.env.DEV) { + console.warn( + `Unknown telemetry event name in disabled list: ${value}` + ) + } + return isValid + }) + ) + } + + trackSignupOpened(): void { + this.trackEvent(TelemetryEvents.USER_SIGN_UP_OPENED) + } + + trackAuth(metadata: AuthMetadata): void { + this.trackEvent(TelemetryEvents.USER_AUTH_COMPLETED, metadata) + } + + trackUserLoggedIn(): void { + this.trackEvent(TelemetryEvents.USER_LOGGED_IN) + } + + trackSubscription( + event: 'modal_opened' | 'subscribe_clicked', + metadata?: SubscriptionMetadata + ): void { + const eventName = + event === 'modal_opened' + ? TelemetryEvents.SUBSCRIPTION_REQUIRED_MODAL_OPENED + : TelemetryEvents.SUBSCRIBE_NOW_BUTTON_CLICKED + + this.trackEvent(eventName, metadata) + } + + trackAddApiCreditButtonClicked(): void { + this.trackEvent(TelemetryEvents.ADD_API_CREDIT_BUTTON_CLICKED) + } + + trackMonthlySubscriptionSucceeded(): void { + this.trackEvent(TelemetryEvents.MONTHLY_SUBSCRIPTION_SUCCEEDED) + } + + trackMonthlySubscriptionCancelled(): void { + this.trackEvent(TelemetryEvents.MONTHLY_SUBSCRIPTION_CANCELLED) + } + + trackApiCreditTopupButtonPurchaseClicked(amount: number): void { + this.trackEvent(TelemetryEvents.API_CREDIT_TOPUP_BUTTON_PURCHASE_CLICKED, { + credit_amount: amount + }) + } + + trackApiCreditTopupSucceeded(): void { + this.trackEvent(TelemetryEvents.API_CREDIT_TOPUP_SUCCEEDED) + } + + trackRunButton(options?: { + subscribe_to_run?: boolean + trigger_source?: ExecutionTriggerSource + }): void { + const executionContext = getExecutionContext() + + const runButtonProperties: RunButtonProperties = { + subscribe_to_run: options?.subscribe_to_run || false, + workflow_type: executionContext.is_template ? 'template' : 'custom', + workflow_name: executionContext.workflow_name ?? 'untitled', + custom_node_count: executionContext.custom_node_count, + total_node_count: executionContext.total_node_count, + subgraph_count: executionContext.subgraph_count, + has_api_nodes: executionContext.has_api_nodes, + api_node_names: executionContext.api_node_names, + has_toolkit_nodes: executionContext.has_toolkit_nodes, + toolkit_node_names: executionContext.toolkit_node_names, + trigger_source: options?.trigger_source + } + + this.lastTriggerSource = options?.trigger_source + this.trackEvent(TelemetryEvents.RUN_BUTTON_CLICKED, runButtonProperties) + } + + trackSurvey( + stage: 'opened' | 'submitted', + responses?: SurveyResponses + ): void { + const eventName = + stage === 'opened' + ? TelemetryEvents.USER_SURVEY_OPENED + : TelemetryEvents.USER_SURVEY_SUBMITTED + + const normalizedResponses = responses + ? normalizeSurveyResponses(responses) + : undefined + + this.trackEvent(eventName, normalizedResponses) + + if ( + stage === 'submitted' && + normalizedResponses && + this.posthog && + this.isEnabled && + !this.disabledEvents.has(TelemetryEvents.USER_SURVEY_SUBMITTED) + ) { + try { + this.posthog.people.set(normalizedResponses) + } catch (error) { + console.error('Failed to set PostHog user properties:', error) + } + } + } + + trackEmailVerification(stage: 'opened' | 'requested' | 'completed'): void { + let eventName: TelemetryEventName + + switch (stage) { + case 'opened': + eventName = TelemetryEvents.USER_EMAIL_VERIFY_OPENED + break + case 'requested': + eventName = TelemetryEvents.USER_EMAIL_VERIFY_REQUESTED + break + case 'completed': + eventName = TelemetryEvents.USER_EMAIL_VERIFY_COMPLETED + break + } + + this.trackEvent(eventName) + } + + trackTemplate(metadata: TemplateMetadata): void { + this.trackEvent(TelemetryEvents.TEMPLATE_WORKFLOW_OPENED, metadata) + } + + trackTemplateLibraryOpened(metadata: TemplateLibraryMetadata): void { + this.trackEvent(TelemetryEvents.TEMPLATE_LIBRARY_OPENED, metadata) + } + + trackTemplateLibraryClosed(metadata: TemplateLibraryClosedMetadata): void { + this.trackEvent(TelemetryEvents.TEMPLATE_LIBRARY_CLOSED, metadata) + } + + trackWorkflowImported(metadata: WorkflowImportMetadata): void { + this.trackEvent(TelemetryEvents.WORKFLOW_IMPORTED, metadata) + } + + trackWorkflowOpened(metadata: WorkflowImportMetadata): void { + this.trackEvent(TelemetryEvents.WORKFLOW_OPENED, metadata) + } + + trackEnterLinear(metadata: EnterLinearMetadata): void { + this.trackEvent(TelemetryEvents.ENTER_LINEAR_MODE, metadata) + } + + trackPageVisibilityChanged(metadata: PageVisibilityMetadata): void { + this.trackEvent(TelemetryEvents.PAGE_VISIBILITY_CHANGED, metadata) + } + + trackTabCount(metadata: TabCountMetadata): void { + this.trackEvent(TelemetryEvents.TAB_COUNT_TRACKING, metadata) + } + + trackNodeSearch(metadata: NodeSearchMetadata): void { + this.trackEvent(TelemetryEvents.NODE_SEARCH, metadata) + } + + trackNodeSearchResultSelected(metadata: NodeSearchResultMetadata): void { + this.trackEvent(TelemetryEvents.NODE_SEARCH_RESULT_SELECTED, metadata) + } + + trackTemplateFilterChanged(metadata: TemplateFilterMetadata): void { + this.trackEvent(TelemetryEvents.TEMPLATE_FILTER_CHANGED, metadata) + } + + trackHelpCenterOpened(metadata: HelpCenterOpenedMetadata): void { + this.trackEvent(TelemetryEvents.HELP_CENTER_OPENED, metadata) + } + + trackHelpResourceClicked(metadata: HelpResourceClickedMetadata): void { + this.trackEvent(TelemetryEvents.HELP_RESOURCE_CLICKED, metadata) + } + + trackHelpCenterClosed(metadata: HelpCenterClosedMetadata): void { + this.trackEvent(TelemetryEvents.HELP_CENTER_CLOSED, metadata) + } + + trackWorkflowCreated(metadata: WorkflowCreatedMetadata): void { + this.trackEvent(TelemetryEvents.WORKFLOW_CREATED, metadata) + } + + trackWorkflowExecution(): void { + const context = getExecutionContext() + const eventContext: ExecutionContext = { + ...context, + trigger_source: this.lastTriggerSource ?? 'unknown' + } + this.trackEvent(TelemetryEvents.EXECUTION_START, eventContext) + this.lastTriggerSource = undefined + } + + trackExecutionError(metadata: ExecutionErrorMetadata): void { + this.trackEvent(TelemetryEvents.EXECUTION_ERROR, metadata) + } + + trackExecutionSuccess(metadata: ExecutionSuccessMetadata): void { + this.trackEvent(TelemetryEvents.EXECUTION_SUCCESS, metadata) + } + + trackSettingChanged(metadata: SettingChangedMetadata): void { + this.trackEvent(TelemetryEvents.SETTING_CHANGED, metadata) + } + + trackUiButtonClicked(metadata: UiButtonClickMetadata): void { + this.trackEvent(TelemetryEvents.UI_BUTTON_CLICKED, metadata) + } + + trackPageView(pageName: string, properties?: PageViewMetadata): void { + this.captureRaw(TelemetryEvents.PAGE_VIEW, { + page_name: pageName, + ...properties + }) + } +} diff --git a/src/platform/telemetry/utils/getExecutionContext.ts b/src/platform/telemetry/utils/getExecutionContext.ts new file mode 100644 index 0000000000..2418ce73e4 --- /dev/null +++ b/src/platform/telemetry/utils/getExecutionContext.ts @@ -0,0 +1,119 @@ +import { + TOOLKIT_BLUEPRINT_MODULES, + TOOLKIT_NODE_NAMES +} from '@/constants/toolkitNodes' +import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' +import { useWorkflowTemplatesStore } from '@/platform/workflow/templates/repositories/workflowTemplatesStore' +import { app } from '@/scripts/app' +import { useNodeDefStore } from '@/stores/nodeDefStore' +import { NodeSourceType } from '@/types/nodeSource' +import { reduceAllNodes } from '@/utils/graphTraversalUtil' + +import type { ExecutionContext } from '../types' + +type NodeMetrics = { + custom_node_count: number + api_node_count: number + toolkit_node_count: number + subgraph_count: number + total_node_count: number + has_api_nodes: boolean + api_node_names: string[] + has_toolkit_nodes: boolean + toolkit_node_names: string[] +} + +export function getExecutionContext(): ExecutionContext { + const workflowStore = useWorkflowStore() + const templatesStore = useWorkflowTemplatesStore() + const nodeDefStore = useNodeDefStore() + const activeWorkflow = workflowStore.activeWorkflow + + const nodeCounts = reduceAllNodes( + app.rootGraph, + (metrics, node) => { + const nodeDef = nodeDefStore.nodeDefsByName[node.type] + const isCustomNode = + nodeDef?.nodeSource?.type === NodeSourceType.CustomNodes + const isApiNode = nodeDef?.api_node === true + const isSubgraph = node.isSubgraphNode?.() === true + + if (isApiNode) { + metrics.has_api_nodes = true + const canonicalName = nodeDef?.name + if (canonicalName && !metrics.api_node_names.includes(canonicalName)) { + metrics.api_node_names.push(canonicalName) + } + } + + const isToolkitNode = + TOOLKIT_NODE_NAMES.has(node.type) || + (nodeDef?.python_module !== undefined && + TOOLKIT_BLUEPRINT_MODULES.has(nodeDef.python_module)) + if (isToolkitNode) { + metrics.has_toolkit_nodes = true + const trackingName = nodeDef?.name ?? node.type + if (!metrics.toolkit_node_names.includes(trackingName)) { + metrics.toolkit_node_names.push(trackingName) + } + } + + metrics.custom_node_count += isCustomNode ? 1 : 0 + metrics.api_node_count += isApiNode ? 1 : 0 + metrics.toolkit_node_count += isToolkitNode ? 1 : 0 + metrics.subgraph_count += isSubgraph ? 1 : 0 + metrics.total_node_count += 1 + + return metrics + }, + { + custom_node_count: 0, + api_node_count: 0, + toolkit_node_count: 0, + subgraph_count: 0, + total_node_count: 0, + has_api_nodes: false, + api_node_names: [], + has_toolkit_nodes: false, + toolkit_node_names: [] + } + ) + + if (activeWorkflow?.filename) { + const isTemplate = templatesStore.knownTemplateNames.has( + activeWorkflow.filename + ) + + if (isTemplate) { + const template = templatesStore.getTemplateByName(activeWorkflow.filename) + + const englishMetadata = templatesStore.getEnglishMetadata( + activeWorkflow.filename + ) + + return { + is_template: true, + workflow_name: activeWorkflow.filename, + template_source: template?.sourceModule, + template_category: englishMetadata?.category ?? template?.category, + template_tags: englishMetadata?.tags ?? template?.tags, + template_models: englishMetadata?.models ?? template?.models, + template_use_case: englishMetadata?.useCase ?? template?.useCase, + template_license: englishMetadata?.license ?? template?.license, + ...nodeCounts + } + } + + return { + is_template: false, + workflow_name: activeWorkflow.filename, + ...nodeCounts + } + } + + return { + is_template: false, + workflow_name: undefined, + ...nodeCounts + } +}