mirror of
https://github.com/Xevion/xevion.dev.git
synced 2026-01-31 00:26:31 -06:00
feat: add PostHog telemetry with type-safe event tracking
Track page views, project interactions, theme changes, external links, PGP actions, and errors. Console logging in dev when PostHog not configured. Requires PUBLIC_POSTHOG_KEY and PUBLIC_POSTHOG_HOST env vars.
This commit is contained in:
+78
-1
@@ -17,6 +17,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"overlayscrollbars": "^2.13.0",
|
||||
"overlayscrollbars-svelte": "^0.5.5",
|
||||
"posthog-js": "^1.321.1",
|
||||
"satori": "^0.18.3",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
},
|
||||
@@ -164,12 +165,58 @@
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
"@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg=="],
|
||||
|
||||
"@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="],
|
||||
|
||||
"@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.208.0", "@opentelemetry/otlp-transformer": "0.208.0", "@opentelemetry/sdk-logs": "0.208.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg=="],
|
||||
|
||||
"@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.208.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.208.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA=="],
|
||||
|
||||
"@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.208.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ=="],
|
||||
|
||||
"@opentelemetry/resources": ["@opentelemetry/resources@2.4.0", "", { "dependencies": { "@opentelemetry/core": "2.4.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-RWvGLj2lMDZd7M/5tjkI/2VHMpXebLgPKvBUd9LRasEWR2xAynDwEYZuLvY9P2NGG73HF07jbbgWX2C9oavcQg=="],
|
||||
|
||||
"@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA=="],
|
||||
|
||||
"@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="],
|
||||
|
||||
"@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="],
|
||||
|
||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.38.0", "", {}, "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg=="],
|
||||
|
||||
"@oxc-project/runtime": ["@oxc-project/runtime@0.71.0", "", {}, "sha512-QwoF5WUXIGFQ+hSxWEib4U/aeLoiDN9JlP18MnBgx9LLPRDfn1iICtcow7Jgey6HLH4XFceWXQD5WBJ39dyJcw=="],
|
||||
|
||||
"@oxc-project/types": ["@oxc-project/types@0.71.0", "", {}, "sha512-5CwQ4MI+P4MQbjLWXgNurA+igGwu/opNetIE13LBs9+V93R64MLvDKOOLZIXSzEfovU3Zef3q3GjPnMTgJTn2w=="],
|
||||
|
||||
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
||||
|
||||
"@posthog/core": ["@posthog/core@1.9.1", "", { "dependencies": { "cross-spawn": "^7.0.6" } }, "sha512-kRb1ch2dhQjsAapZmu6V66551IF2LnCbc1rnrQqnR7ArooVyJN9KOPXre16AJ3ObJz2eTfuP7x25BMyS2Y5Exw=="],
|
||||
|
||||
"@posthog/types": ["@posthog/types@1.321.1", "", {}, "sha512-GPVGSvP/uLk/VLQBOnLdRhCoLmce2r0YQ4j4X2aC0fRvPYepJC+O8S0ESHg5vZW4abGrxQktaCzx7jFZur5lBw=="],
|
||||
|
||||
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
|
||||
|
||||
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
|
||||
|
||||
"@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="],
|
||||
|
||||
"@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="],
|
||||
|
||||
"@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="],
|
||||
|
||||
"@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
|
||||
|
||||
"@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="],
|
||||
|
||||
"@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
|
||||
|
||||
"@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
|
||||
|
||||
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="],
|
||||
|
||||
"@resvg/resvg-js": ["@resvg/resvg-js@2.6.2", "", { "optionalDependencies": { "@resvg/resvg-js-android-arm-eabi": "2.6.2", "@resvg/resvg-js-android-arm64": "2.6.2", "@resvg/resvg-js-darwin-arm64": "2.6.2", "@resvg/resvg-js-darwin-x64": "2.6.2", "@resvg/resvg-js-linux-arm-gnueabihf": "2.6.2", "@resvg/resvg-js-linux-arm64-gnu": "2.6.2", "@resvg/resvg-js-linux-arm64-musl": "2.6.2", "@resvg/resvg-js-linux-x64-gnu": "2.6.2", "@resvg/resvg-js-linux-x64-musl": "2.6.2", "@resvg/resvg-js-win32-arm64-msvc": "2.6.2", "@resvg/resvg-js-win32-ia32-msvc": "2.6.2", "@resvg/resvg-js-win32-x64-msvc": "2.6.2" } }, "sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q=="],
|
||||
|
||||
"@resvg/resvg-js-android-arm-eabi": ["@resvg/resvg-js-android-arm-eabi@2.6.2", "", { "os": "android", "cpu": "arm" }, "sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA=="],
|
||||
@@ -318,6 +365,8 @@
|
||||
|
||||
"@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
|
||||
|
||||
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.51.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/type-utils": "8.51.0", "@typescript-eslint/utils": "8.51.0", "@typescript-eslint/visitor-keys": "8.51.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.2.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.51.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og=="],
|
||||
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.51.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/types": "8.51.0", "@typescript-eslint/typescript-estree": "8.51.0", "@typescript-eslint/visitor-keys": "8.51.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A=="],
|
||||
@@ -396,6 +445,8 @@
|
||||
|
||||
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
|
||||
|
||||
"core-js": ["core-js@3.47.0", "", {}, "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"css-background-parser": ["css-background-parser@0.1.0", "", {}, "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA=="],
|
||||
@@ -420,6 +471,8 @@
|
||||
|
||||
"devalue": ["devalue@5.6.1", "", {}, "sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A=="],
|
||||
|
||||
"dompurify": ["dompurify@3.3.1", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q=="],
|
||||
|
||||
"dot-case": ["dot-case@3.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
@@ -472,7 +525,7 @@
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"fflate": ["fflate@0.7.4", "", {}, "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw=="],
|
||||
"fflate": ["fflate@0.4.8", "", {}, "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="],
|
||||
|
||||
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
||||
|
||||
@@ -568,6 +621,8 @@
|
||||
|
||||
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
||||
|
||||
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
||||
|
||||
"lower-case": ["lower-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
@@ -634,16 +689,24 @@
|
||||
|
||||
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||
|
||||
"posthog-js": ["posthog-js@1.321.1", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/api-logs": "^0.208.0", "@opentelemetry/exporter-logs-otlp-http": "^0.208.0", "@opentelemetry/resources": "^2.2.0", "@opentelemetry/sdk-logs": "^0.208.0", "@posthog/core": "1.9.1", "@posthog/types": "1.321.1", "core-js": "^3.38.1", "dompurify": "^3.3.1", "fflate": "^0.4.8", "preact": "^10.28.0", "query-selector-shadow-dom": "^1.0.1", "web-vitals": "^4.2.4" } }, "sha512-PsRkY3LvPg3VUKROvc3HwSGUU9/JKHx/cNS4rZ5uHD5tDRACjjguGgFTp9XBLuf/+9YZcfdg1wxidk5bODvc2w=="],
|
||||
|
||||
"preact": ["preact@10.28.2", "", {}, "sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA=="],
|
||||
|
||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||
|
||||
"prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="],
|
||||
|
||||
"prettier-plugin-svelte": ["prettier-plugin-svelte@3.4.1", "", { "peerDependencies": { "prettier": "^3.0.0", "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" } }, "sha512-xL49LCloMoZRvSwa6IEdN2GV6cq2IqpYGstYtMT+5wmml1/dClEoI0MZR78MiVPpu6BdQFfN0/y73yO6+br5Pg=="],
|
||||
|
||||
"protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="],
|
||||
|
||||
"query-selector-shadow-dom": ["query-selector-shadow-dom@1.0.1", "", {}, "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw=="],
|
||||
|
||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||
|
||||
"relateurl": ["relateurl@0.2.7", "", {}, "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog=="],
|
||||
@@ -746,6 +809,8 @@
|
||||
|
||||
"vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
|
||||
|
||||
"web-vitals": ["web-vitals@4.2.4", "", {}, "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw=="],
|
||||
|
||||
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
@@ -772,6 +837,18 @@
|
||||
|
||||
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
||||
|
||||
"@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="],
|
||||
|
||||
"@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.4.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw=="],
|
||||
|
||||
"@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="],
|
||||
|
||||
"@opentelemetry/sdk-metrics/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="],
|
||||
|
||||
"@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="],
|
||||
|
||||
"@shuding/opentype.js/fflate": ["fflate@0.7.4", "", {}, "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"overlayscrollbars": "^2.13.0",
|
||||
"overlayscrollbars-svelte": "^0.5.5",
|
||||
"posthog-js": "^1.321.1",
|
||||
"satori": "^0.18.3",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { HandleClientError } from "@sveltejs/kit";
|
||||
import { telemetry } from "$lib/telemetry";
|
||||
|
||||
export const handleError: HandleClientError = ({ error, status, message }) => {
|
||||
telemetry.trackError(
|
||||
status >= 500 ? "runtime_error" : "network_error",
|
||||
message,
|
||||
{
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
context: { status },
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
message: status === 404 ? "Not Found" : "An error occurred",
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
import { telemetry } from "$lib/telemetry";
|
||||
import TagChip from "./TagChip.svelte";
|
||||
import type { AdminProject } from "$lib/admin-types";
|
||||
|
||||
@@ -21,6 +22,25 @@
|
||||
|
||||
const isLink = $derived(!!projectUrl);
|
||||
|
||||
// Determine click action type for telemetry
|
||||
const clickAction = $derived(
|
||||
project.demoUrl ? "demo_click" : project.githubRepo ? "github_click" : null,
|
||||
);
|
||||
|
||||
function handleClick() {
|
||||
if (clickAction && projectUrl) {
|
||||
telemetry.track({
|
||||
name: "project_interaction",
|
||||
properties: {
|
||||
action: clickAction,
|
||||
projectSlug: project.slug,
|
||||
projectName: project.name,
|
||||
targetUrl: projectUrl,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
@@ -44,6 +64,8 @@
|
||||
href={isLink ? projectUrl : undefined}
|
||||
target={isLink ? "_blank" : undefined}
|
||||
rel={isLink ? "noopener noreferrer" : undefined}
|
||||
onclick={handleClick}
|
||||
role={isLink ? undefined : "article"}
|
||||
class={cn(
|
||||
"flex h-44 flex-col gap-2.5 rounded-lg border border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/50 p-3",
|
||||
isLink &&
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { themeStore } from "$lib/stores/theme.svelte";
|
||||
import { telemetry } from "$lib/telemetry";
|
||||
import IconSun from "~icons/lucide/sun";
|
||||
import IconMoon from "~icons/lucide/moon";
|
||||
|
||||
function handleToggle() {
|
||||
const newTheme = themeStore.isDark ? "light" : "dark";
|
||||
themeStore.toggle();
|
||||
telemetry.track({
|
||||
name: "theme_change",
|
||||
properties: { theme: newTheme },
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => themeStore.toggle()}
|
||||
onclick={handleToggle}
|
||||
aria-label={themeStore.isDark
|
||||
? "Switch to light mode"
|
||||
: "Switch to dark mode"}
|
||||
|
||||
@@ -32,6 +32,8 @@ export class ApiError extends Error {
|
||||
* Check if an error is a server error (5xx)
|
||||
*/
|
||||
static isServerError(error: unknown): boolean {
|
||||
return error instanceof ApiError && error.status >= 500 && error.status < 600;
|
||||
return (
|
||||
error instanceof ApiError && error.status >= 500 && error.status < 600
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { telemetry } from "$lib/telemetry";
|
||||
|
||||
class AuthStore {
|
||||
isAuthenticated = $state(false);
|
||||
username = $state<string | null>(null);
|
||||
@@ -17,6 +19,7 @@ class AuthStore {
|
||||
const data = await response.json();
|
||||
this.isAuthenticated = true;
|
||||
this.username = data.username;
|
||||
telemetry.identifyAdmin(data.username);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -38,6 +41,7 @@ class AuthStore {
|
||||
} finally {
|
||||
this.isAuthenticated = false;
|
||||
this.username = null;
|
||||
telemetry.reset();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* Telemetry client wrapper for PostHog with type-safe event tracking.
|
||||
* Provides console logging in development when PostHog is not configured.
|
||||
*/
|
||||
|
||||
import posthog from "posthog-js";
|
||||
import { browser, dev } from "$app/environment";
|
||||
import { env } from "$env/dynamic/public";
|
||||
import type { TelemetryEvent, ExternalLinkEvent, ErrorEvent } from "./events";
|
||||
|
||||
// Environment variables for PostHog configuration
|
||||
// Set PUBLIC_POSTHOG_KEY and PUBLIC_POSTHOG_HOST in your .env file
|
||||
// Using dynamic/public so they can be set at runtime (not just build time)
|
||||
const POSTHOG_KEY = env.PUBLIC_POSTHOG_KEY;
|
||||
const POSTHOG_HOST = env.PUBLIC_POSTHOG_HOST;
|
||||
|
||||
class TelemetryClient {
|
||||
private initialized = false;
|
||||
private enabled = false;
|
||||
|
||||
/**
|
||||
* Centralized logging method that only logs in development mode
|
||||
*/
|
||||
private log(message: string, data?: unknown): void {
|
||||
if (dev) {
|
||||
if (data !== undefined) {
|
||||
console.log(`[Telemetry] ${message}`, data);
|
||||
} else {
|
||||
console.log(`[Telemetry] ${message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the PostHog client if keys are available
|
||||
*/
|
||||
init(): void {
|
||||
if (this.initialized || !browser) return;
|
||||
|
||||
// Only enable PostHog if both key and host are configured
|
||||
if (POSTHOG_KEY && POSTHOG_HOST) {
|
||||
posthog.init(POSTHOG_KEY, {
|
||||
api_host: POSTHOG_HOST,
|
||||
ui_host: "https://us.posthog.com", // For toolbar links
|
||||
capture_pageview: false, // We handle page views manually
|
||||
capture_pageleave: true,
|
||||
autocapture: true,
|
||||
persistence: "localStorage",
|
||||
// Session replay config
|
||||
session_recording: {
|
||||
recordCrossOriginIframes: true,
|
||||
},
|
||||
});
|
||||
|
||||
this.enabled = true;
|
||||
this.log("PostHog initialized");
|
||||
|
||||
if (dev) {
|
||||
posthog.debug();
|
||||
}
|
||||
} else {
|
||||
this.enabled = false;
|
||||
this.log(
|
||||
"PostHog not configured (missing PUBLIC_POSTHOG_KEY or PUBLIC_POSTHOG_HOST)",
|
||||
);
|
||||
}
|
||||
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track a telemetry event with type safety
|
||||
*/
|
||||
track<E extends TelemetryEvent>(event: E): void {
|
||||
if (!browser) return;
|
||||
|
||||
this.log(event.name, event.properties);
|
||||
|
||||
if (this.enabled) {
|
||||
posthog.capture(event.name, event.properties);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for tracking page views
|
||||
*/
|
||||
trackPageView(route: string): void {
|
||||
this.track({
|
||||
name: "page_view",
|
||||
properties: {
|
||||
route,
|
||||
referrer: browser ? document.referrer : undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Identify a user with properties
|
||||
*/
|
||||
identify(userId: string, properties?: Record<string, unknown>): void {
|
||||
if (!browser) return;
|
||||
|
||||
this.log("identify", { userId, properties });
|
||||
|
||||
if (this.enabled) {
|
||||
posthog.identify(userId, properties);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset user identification (e.g., on logout)
|
||||
*/
|
||||
reset(): void {
|
||||
if (!this.initialized || !browser) return;
|
||||
|
||||
this.log("reset");
|
||||
|
||||
if (this.enabled) {
|
||||
posthog.reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if telemetry is enabled
|
||||
*/
|
||||
isEnabled(): boolean {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identify an admin user with admin flag
|
||||
*/
|
||||
identifyAdmin(username: string): void {
|
||||
this.identify(username, {
|
||||
is_admin: true,
|
||||
admin_username: username,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Track an external link click
|
||||
*/
|
||||
trackExternalLink(
|
||||
url: string,
|
||||
context: ExternalLinkEvent["properties"]["context"],
|
||||
): void {
|
||||
this.track({
|
||||
name: "external_link_click",
|
||||
properties: { url, context },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Track an error event
|
||||
*/
|
||||
trackError(
|
||||
errorType: ErrorEvent["properties"]["errorType"],
|
||||
message: string,
|
||||
options?: { stack?: string; context?: Record<string, unknown> },
|
||||
): void {
|
||||
this.track({
|
||||
name: "error",
|
||||
properties: {
|
||||
errorType,
|
||||
message,
|
||||
stack: options?.stack,
|
||||
context: options?.context,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Singleton telemetry client instance
|
||||
*/
|
||||
export const telemetry = new TelemetryClient();
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Type-safe telemetry event system using discriminated unions.
|
||||
* All events must have a 'name' discriminator property.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Page view tracking event
|
||||
*/
|
||||
export type PageViewEvent = {
|
||||
name: "page_view";
|
||||
properties: {
|
||||
route: string;
|
||||
referrer?: string;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Project card interaction event
|
||||
*/
|
||||
export type ProjectInteractionEvent = {
|
||||
name: "project_interaction";
|
||||
properties: {
|
||||
action: "card_view" | "github_click" | "demo_click";
|
||||
projectSlug: string;
|
||||
projectName: string;
|
||||
targetUrl?: string;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Tag interaction event (for fuzzy discovery feature)
|
||||
*/
|
||||
export type TagInteractionEvent = {
|
||||
name: "tag_interaction";
|
||||
properties: {
|
||||
action: "select" | "deselect" | "reset";
|
||||
tagSlug?: string;
|
||||
selectedTags: string[];
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* External link click event
|
||||
*/
|
||||
export type ExternalLinkEvent = {
|
||||
name: "external_link_click";
|
||||
properties: {
|
||||
url: string;
|
||||
context: "social" | "project" | "footer" | "pgp" | "resume";
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Theme preference change event
|
||||
*/
|
||||
export type ThemeEvent = {
|
||||
name: "theme_change";
|
||||
properties: {
|
||||
theme: "light" | "dark";
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Error tracking event
|
||||
*/
|
||||
export type ErrorEvent = {
|
||||
name: "error";
|
||||
properties: {
|
||||
errorType: "network_error" | "validation_error" | "runtime_error" | string;
|
||||
message: string;
|
||||
stack?: string;
|
||||
context?: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* PGP page interaction event
|
||||
*/
|
||||
export type PgpInteractionEvent = {
|
||||
name: "pgp_interaction";
|
||||
properties: {
|
||||
action: "copy_key" | "download_key" | "copy_command";
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Discriminated union of all possible events
|
||||
*/
|
||||
export type TelemetryEvent =
|
||||
| PageViewEvent
|
||||
| ProjectInteractionEvent
|
||||
| TagInteractionEvent
|
||||
| ExternalLinkEvent
|
||||
| ThemeEvent
|
||||
| ErrorEvent
|
||||
| PgpInteractionEvent;
|
||||
|
||||
/**
|
||||
* Helper type to extract event properties by event name
|
||||
*/
|
||||
export type EventProperties<T extends TelemetryEvent["name"]> = Extract<
|
||||
TelemetryEvent,
|
||||
{ name: T }
|
||||
>["properties"];
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Telemetry module exports
|
||||
*/
|
||||
|
||||
export { telemetry } from "./client";
|
||||
export type {
|
||||
TelemetryEvent,
|
||||
PageViewEvent,
|
||||
ProjectInteractionEvent,
|
||||
TagInteractionEvent,
|
||||
ExternalLinkEvent,
|
||||
ThemeEvent,
|
||||
ErrorEvent,
|
||||
PgpInteractionEvent,
|
||||
EventProperties,
|
||||
} from "./events";
|
||||
@@ -8,7 +8,8 @@
|
||||
import { onMount } from "svelte";
|
||||
import { themeStore } from "$lib/stores/theme.svelte";
|
||||
import { page } from "$app/stores";
|
||||
import { onNavigate } from "$app/navigation";
|
||||
import { afterNavigate, onNavigate } from "$app/navigation";
|
||||
import { telemetry } from "$lib/telemetry";
|
||||
import Clouds from "$lib/components/Clouds.svelte";
|
||||
import Dots from "$lib/components/Dots.svelte";
|
||||
import ThemeToggle from "$lib/components/ThemeToggle.svelte";
|
||||
@@ -60,6 +61,13 @@
|
||||
});
|
||||
});
|
||||
|
||||
// Track page views on navigation (SPA navigations)
|
||||
afterNavigate(({ to }) => {
|
||||
if (to?.url.pathname) {
|
||||
telemetry.trackPageView(to.url.pathname);
|
||||
}
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
// Detect if this is a page reload (F5 or CTRL+F5) vs initial load or SPA navigation
|
||||
const navigation = performance.getEntriesByType(
|
||||
@@ -76,6 +84,9 @@
|
||||
// Initialize theme store
|
||||
themeStore.init();
|
||||
|
||||
// Initialize PostHog telemetry (page views tracked via afterNavigate)
|
||||
telemetry.init();
|
||||
|
||||
// Initialize overlay scrollbars on the body element
|
||||
const osInstance = OverlayScrollbars(document.body, {
|
||||
scrollbars: {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { page } from "$app/state";
|
||||
import ProjectCard from "$lib/components/ProjectCard.svelte";
|
||||
import DiscordProfileModal from "$lib/components/DiscordProfileModal.svelte";
|
||||
import { telemetry } from "$lib/telemetry";
|
||||
import type { PageData } from "./$types";
|
||||
import MaterialSymbolsVpnKey from "~icons/material-symbols/vpn-key";
|
||||
|
||||
@@ -31,6 +32,10 @@
|
||||
function openDiscordModal(username: string) {
|
||||
pushState("", { discordModal: { open: true, username } });
|
||||
}
|
||||
|
||||
function trackSocialClick(url: string) {
|
||||
telemetry.trackExternalLink(url, "social");
|
||||
}
|
||||
</script>
|
||||
|
||||
<main class="page-main overflow-x-hidden font-schibsted">
|
||||
@@ -64,6 +69,7 @@
|
||||
<!-- Simple link platforms -->
|
||||
<a
|
||||
href={link.value}
|
||||
onclick={() => trackSocialClick(link.value)}
|
||||
class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-100 dark:bg-zinc-900 shadow-sm hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 dark:focus-visible:ring-zinc-500 cursor-pointer"
|
||||
>
|
||||
<span class="size-4 text-zinc-600 dark:text-zinc-300">
|
||||
@@ -80,7 +86,10 @@
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-100 dark:bg-zinc-900 shadow-sm hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 dark:focus-visible:ring-zinc-500 cursor-pointer"
|
||||
onclick={() => openDiscordModal(link.value)}
|
||||
onclick={() => {
|
||||
trackSocialClick(`discord:${link.value}`);
|
||||
openDiscordModal(link.value);
|
||||
}}
|
||||
>
|
||||
<span class="size-4 text-zinc-600 dark:text-zinc-300">
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
@@ -95,6 +104,7 @@
|
||||
<!-- Email - mailto link -->
|
||||
<a
|
||||
href="mailto:{link.value}"
|
||||
onclick={() => trackSocialClick(`mailto:${link.value}`)}
|
||||
class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-100 dark:bg-zinc-900 shadow-sm hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 dark:focus-visible:ring-zinc-500 cursor-pointer"
|
||||
>
|
||||
<span class="size-4.5 text-zinc-600 dark:text-zinc-300">
|
||||
@@ -108,7 +118,7 @@
|
||||
</a>
|
||||
{/if}
|
||||
{/each}
|
||||
<!-- PGP Key - links to dedicated page -->
|
||||
<!-- PGP Key - links to dedicated page (tracked via page view) -->
|
||||
<a
|
||||
href="/pgp"
|
||||
class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-100 dark:bg-zinc-900 shadow-sm hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 dark:focus-visible:ring-zinc-500 cursor-pointer"
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import Sidebar from "$lib/components/admin/Sidebar.svelte";
|
||||
import { authStore } from "$lib/stores/auth.svelte";
|
||||
import { getAdminStats } from "$lib/api";
|
||||
import { telemetry } from "$lib/telemetry";
|
||||
import type { AdminStats } from "$lib/admin-types";
|
||||
|
||||
let { children, data } = $props();
|
||||
@@ -33,6 +34,7 @@
|
||||
!authStore.isAuthenticated
|
||||
) {
|
||||
authStore.setSession(data.session.username);
|
||||
telemetry.identifyAdmin(data.session.username);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ export const load: PageServerLoad = async ({ params, fetch }) => {
|
||||
`/api/tags/${slug}/related`,
|
||||
{ fetch },
|
||||
);
|
||||
} catch (err) {
|
||||
} catch {
|
||||
// Non-fatal - just show empty related tags
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-svelte";
|
||||
import { telemetry } from "$lib/telemetry";
|
||||
import IconDownload from "~icons/material-symbols/download-rounded";
|
||||
import IconCopy from "~icons/material-symbols/content-copy-rounded";
|
||||
import IconCheck from "~icons/material-symbols/check-rounded";
|
||||
@@ -11,6 +12,10 @@
|
||||
let copyCommandSuccess = $state(false);
|
||||
|
||||
async function copyToClipboard() {
|
||||
telemetry.track({
|
||||
name: "pgp_interaction",
|
||||
properties: { action: "copy_key" },
|
||||
});
|
||||
try {
|
||||
await navigator.clipboard.writeText(data.key.content);
|
||||
copySuccess = true;
|
||||
@@ -23,6 +28,10 @@
|
||||
}
|
||||
|
||||
async function copyCommand() {
|
||||
telemetry.track({
|
||||
name: "pgp_interaction",
|
||||
properties: { action: "copy_command" },
|
||||
});
|
||||
try {
|
||||
await navigator.clipboard.writeText(
|
||||
"curl https://xevion.dev/pgp | gpg --import",
|
||||
@@ -37,6 +46,10 @@
|
||||
}
|
||||
|
||||
function downloadKey() {
|
||||
telemetry.track({
|
||||
name: "pgp_interaction",
|
||||
properties: { action: "download_key" },
|
||||
});
|
||||
const a = document.createElement("a");
|
||||
a.href = "/publickey.asc";
|
||||
a.download = "publickey.asc";
|
||||
|
||||
@@ -14,6 +14,9 @@ const config = {
|
||||
alias: {
|
||||
$components: "src/lib/components",
|
||||
},
|
||||
paths: {
|
||||
relative: false, // Required for PostHog session replay with SSR
|
||||
},
|
||||
prerender: {
|
||||
handleHttpError: ({ path, referrer, message }) => {
|
||||
console.log(
|
||||
|
||||
Reference in New Issue
Block a user