feat: add PostHog telemetry with privacy-focused tracking

Integrate optional PostHog telemetry to track page views, RDAP queries,
user interactions, and errors while maintaining user privacy.

Key features:
- Type-safe event tracking with discriminated unions
- Automatic source map upload for production error tracking
- Privacy protections (query targets excluded from successful lookups)
- Do Not Track (DNT) header support
- Optional telemetry (disabled by default without environment variables)
- Error boundary with automatic error tracking
- Context-based telemetry integration throughout UI components

Environment variables required for telemetry:
- NEXT_PUBLIC_POSTHOG_KEY: PostHog project API key (client-side)
- NEXT_PUBLIC_POSTHOG_HOST: PostHog API endpoint (client-side)
- POSTHOG_PERSONAL_API_KEY: Source map upload key (server-side)
This commit is contained in:
2025-10-23 18:00:24 -05:00
parent 5fcf9dd94b
commit 5fde7d249f
15 changed files with 858 additions and 17 deletions

View File

@@ -169,6 +169,14 @@ To self-host:
pnpm build pnpm build
``` ```
## Privacy & Telemetry
The hosted demo at [rdap.xevion.dev][live-demo] collects optional telemetry to improve the service. Self-hosted deployments have no telemetry by default.
**What's tracked:** Page views, query metadata (type, success/failure, timing), user interactions, and errors.
**Privacy protections:** Successful query targets are never logged—only the query type and timing. Failed queries may include targets for debugging. Copy actions track text length only.
## Contributing ## Contributing
Issues and pull requests are welcome! This project uses: Issues and pull requests are welcome! This project uses:

View File

@@ -1,4 +1,6 @@
// @ts-check // @ts-check
import { withPostHogConfig } from "@posthog/nextjs-config";
/** /**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation.
* This is especially useful for Docker builds. * This is especially useful for Docker builds.
@@ -6,11 +8,27 @@
!process.env.SKIP_ENV_VALIDATION && (await import("./src/env/server.mjs")); !process.env.SKIP_ENV_VALIDATION && (await import("./src/env/server.mjs"));
/** @type {import("next").NextConfig} */ /** @type {import("next").NextConfig} */
const config = { const nextConfig = {
reactStrictMode: true, reactStrictMode: true,
i18n: { i18n: {
locales: ["en"], locales: ["en"],
defaultLocale: "en", defaultLocale: "en",
}, },
}; };
export default config;
// PostHog source map configuration
// Only uploads source maps in production builds when POSTHOG_PERSONAL_API_KEY is set
const postHogConfig = {
personalApiKey: process.env.POSTHOG_PERSONAL_API_KEY || "",
envId: "238067", // PostHog project ID for source map correlation
host: "https://us.i.posthog.com",
sourcemaps: {
// Only enable on production builds with API key
enabled: process.env.NODE_ENV === "production" && !!process.env.POSTHOG_PERSONAL_API_KEY,
project: "rdap",
version: process.env.VERCEL_GIT_COMMIT_SHA || "local",
deleteAfterUpload: true,
},
};
export default withPostHogConfig(nextConfig, postHogConfig);

View File

@@ -29,6 +29,7 @@
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"overlayscrollbars": "^2.12.0", "overlayscrollbars": "^2.12.0",
"overlayscrollbars-react": "^0.5.6", "overlayscrollbars-react": "^0.5.6",
"posthog-js": "^1.280.1",
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0", "react-dom": "19.2.0",
"react-hook-form": "^7.42.1", "react-hook-form": "^7.42.1",
@@ -43,6 +44,7 @@
"@codecov/vite-plugin": "^1.9.1", "@codecov/vite-plugin": "^1.9.1",
"@commitlint/cli": "^19.0.0", "@commitlint/cli": "^19.0.0",
"@commitlint/config-conventional": "^19.0.0", "@commitlint/config-conventional": "^19.0.0",
"@posthog/nextjs-config": "^1.3.6",
"@tailwindcss/postcss": "^4.1.15", "@tailwindcss/postcss": "^4.1.15",
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",

316
pnpm-lock.yaml generated
View File

@@ -50,6 +50,9 @@ importers:
overlayscrollbars-react: overlayscrollbars-react:
specifier: ^0.5.6 specifier: ^0.5.6
version: 0.5.6(overlayscrollbars@2.12.0)(react@19.2.0) version: 0.5.6(overlayscrollbars@2.12.0)(react@19.2.0)
posthog-js:
specifier: ^1.280.1
version: 1.280.1
react: react:
specifier: 19.2.0 specifier: 19.2.0
version: 19.2.0 version: 19.2.0
@@ -87,6 +90,9 @@ importers:
'@commitlint/config-conventional': '@commitlint/config-conventional':
specifier: ^19.0.0 specifier: ^19.0.0
version: 19.8.1 version: 19.8.1
'@posthog/nextjs-config':
specifier: ^1.3.6
version: 1.3.6(next@15.5.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.93.2))
'@tailwindcss/postcss': '@tailwindcss/postcss':
specifier: ^4.1.15 specifier: ^4.1.15
version: 4.1.15 version: 4.1.15
@@ -653,6 +659,18 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@isaacs/balanced-match@4.0.1':
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
engines: {node: 20 || >=22}
'@isaacs/brace-expansion@5.0.0':
resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==}
engines: {node: 20 || >=22}
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
'@jridgewell/gen-mapping@0.3.13': '@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
@@ -880,6 +898,20 @@ packages:
'@polka/url@1.0.0-next.29': '@polka/url@1.0.0-next.29':
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
'@posthog/cli@0.5.5':
resolution: {integrity: sha512-Cs0xA4W6piyrBUQbCahbSvHUIE1qU9xca4TfVMKhHjbfEJBICV+iLi6TxrQf4REijT+4ln6NL7KF4wbVy0DJUQ==}
engines: {node: '>=14', npm: '>=6'}
hasBin: true
'@posthog/core@1.3.1':
resolution: {integrity: sha512-sGKVHituJ8L/bJxVV4KamMFp+IBWAZyCiYunFawJZ4cc59PCtLnKFIMEV6kn7A4eZQcQ6EKV5Via4sF3Z7qMLQ==}
'@posthog/nextjs-config@1.3.6':
resolution: {integrity: sha512-Xs6s+ol0Rkmf1cSMfEIhegeMiHIlqb8JVOe/yoIet9BKWywzoYFa5xKwIjsLe0tmnrcitONiGRl/yyHULKNFtw==}
engines: {node: '>=20'}
peerDependencies:
next: '>12.1.0'
'@radix-ui/colors@3.0.0': '@radix-ui/colors@3.0.0':
resolution: {integrity: sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==} resolution: {integrity: sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==}
@@ -2172,6 +2204,9 @@ packages:
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
available-typed-arrays@1.0.7: available-typed-arrays@1.0.7:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -2180,6 +2215,12 @@ packages:
resolution: {integrity: sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==} resolution: {integrity: sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==}
engines: {node: '>=4'} engines: {node: '>=4'}
axios-proxy-builder@0.1.2:
resolution: {integrity: sha512-6uBVsBZzkB3tCC8iyx59mCjQckhB8+GQrI9Cop8eC7ybIsvs/KtnNgEBfRMSEa7GqK2VBGUzgjNYMdPIfotyPA==}
axios@1.12.2:
resolution: {integrity: sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==}
axobject-query@4.1.0: axobject-query@4.1.0:
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -2253,6 +2294,10 @@ packages:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
clone@1.0.4:
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
engines: {node: '>=0.8'}
clsx@2.1.1: clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -2267,6 +2312,10 @@ packages:
colorette@2.0.20: colorette@2.0.20:
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
commander@13.1.0: commander@13.1.0:
resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -2277,6 +2326,10 @@ packages:
concat-map@0.0.1: concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
console.table@0.10.0:
resolution: {integrity: sha512-dPyZofqggxuvSf7WXvNjuRfnsOk1YazkVP8FdxH4tcH2c37wc79/Yl6Bhr7Lsu00KMgy2ql/qCMuNu8xctZM8g==}
engines: {node: '> 0.10'}
conventional-changelog-angular@7.0.0: conventional-changelog-angular@7.0.0:
resolution: {integrity: sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==} resolution: {integrity: sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==}
engines: {node: '>=16'} engines: {node: '>=16'}
@@ -2290,6 +2343,9 @@ packages:
engines: {node: '>=16'} engines: {node: '>=16'}
hasBin: true hasBin: true
core-js@3.46.0:
resolution: {integrity: sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==}
cosmiconfig-typescript-loader@6.2.0: cosmiconfig-typescript-loader@6.2.0:
resolution: {integrity: sha512-GEN39v7TgdxgIoNcdkRE3uiAzQt3UXLyHbRHD6YoL048XAeOomyxaP+Hh/+2C6C2wYjxJ2onhJcsQp+L4YEkVQ==} resolution: {integrity: sha512-GEN39v7TgdxgIoNcdkRE3uiAzQt3UXLyHbRHD6YoL048XAeOomyxaP+Hh/+2C6C2wYjxJ2onhJcsQp+L4YEkVQ==}
engines: {node: '>=v18'} engines: {node: '>=v18'}
@@ -2364,6 +2420,9 @@ packages:
deep-is@0.1.4: deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
defaults@1.0.4:
resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==}
define-data-property@1.1.4: define-data-property@1.1.4:
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -2372,6 +2431,10 @@ packages:
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
deprecation@2.3.1: deprecation@2.3.1:
resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==}
@@ -2409,6 +2472,12 @@ packages:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
easy-table@1.1.0:
resolution: {integrity: sha512-oq33hWOSSnl2Hoh00tZWaIPi1ievrD9aFG82/IgjlycAnW9hHx5PkJiXpxPsgEE+H7BsbVQXFVFST8TEXS6/pA==}
emoji-regex@10.6.0: emoji-regex@10.6.0:
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
@@ -2643,6 +2712,9 @@ packages:
picomatch: picomatch:
optional: true optional: true
fflate@0.4.8:
resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
fflate@0.8.2: fflate@0.8.2:
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
@@ -2669,10 +2741,27 @@ packages:
flatted@3.3.3: flatted@3.3.3:
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
follow-redirects@1.15.11:
resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
for-each@0.3.5: for-each@0.3.5:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
foreground-child@3.3.1:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
form-data@4.0.4:
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
engines: {node: '>= 6'}
fsevents@2.3.3: fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -2736,6 +2825,11 @@ packages:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
glob@11.0.3:
resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==}
engines: {node: 20 || >=22}
hasBin: true
global-directory@4.0.1: global-directory@4.0.1:
resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -2991,6 +3085,10 @@ packages:
resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
jackspeak@4.1.1:
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
engines: {node: 20 || >=22}
jiti@2.6.1: jiti@2.6.1:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true hasBin: true
@@ -3178,6 +3276,10 @@ packages:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true hasBin: true
lru-cache@11.2.2:
resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==}
engines: {node: 20 || >=22}
lz-string@1.5.0: lz-string@1.5.0:
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
hasBin: true hasBin: true
@@ -3211,6 +3313,14 @@ packages:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'} engines: {node: '>=8.6'}
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
mimic-fn@4.0.0: mimic-fn@4.0.0:
resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -3223,6 +3333,10 @@ packages:
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
engines: {node: '>=4'} engines: {node: '>=4'}
minimatch@10.0.3:
resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==}
engines: {node: 20 || >=22}
minimatch@3.1.2: minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
@@ -3233,6 +3347,10 @@ packages:
minimist@1.2.8: minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
minipass@7.1.2:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
mrmime@2.0.1: mrmime@2.0.1:
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -3363,6 +3481,9 @@ packages:
resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
parent-module@1.0.1: parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -3390,6 +3511,10 @@ packages:
path-parse@1.0.7: path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
path-scurry@2.0.0:
resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==}
engines: {node: 20 || >=22}
pathe@2.0.3: pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
@@ -3421,6 +3546,12 @@ packages:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
posthog-js@1.280.1:
resolution: {integrity: sha512-ZfXaExn42aZBQg88pBzGo71sy4hmei3ubBPb7DBbsfYC3ipZ6F6xmNr3ScNdOyF8cBTZbG24SlIvntp83jTlUA==}
preact@10.27.2:
resolution: {integrity: sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==}
prelude-ls@1.2.1: prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@@ -3492,6 +3623,9 @@ packages:
prop-types@15.8.1: prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
punycode@2.3.1: punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -3623,6 +3757,11 @@ packages:
rfdc@1.4.1: rfdc@1.4.1:
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
rimraf@6.0.1:
resolution: {integrity: sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==}
engines: {node: 20 || >=22}
hasBin: true
rollup@4.52.5: rollup@4.52.5:
resolution: {integrity: sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==} resolution: {integrity: sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'} engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@@ -3748,6 +3887,10 @@ packages:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'} engines: {node: '>=8'}
string-width@5.1.2:
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
engines: {node: '>=12'}
string-width@7.2.0: string-width@7.2.0:
resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -4043,6 +4186,12 @@ packages:
jsdom: jsdom:
optional: true optional: true
wcwidth@1.0.1:
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
web-vitals@4.2.4:
resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==}
webpack-virtual-modules@0.6.2: webpack-virtual-modules@0.6.2:
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
@@ -4084,6 +4233,10 @@ packages:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
wrap-ansi@8.1.0:
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
engines: {node: '>=12'}
wrap-ansi@9.0.2: wrap-ansi@9.0.2:
resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -4565,6 +4718,21 @@ snapshots:
'@img/sharp-win32-x64@0.34.4': '@img/sharp-win32-x64@0.34.4':
optional: true optional: true
'@isaacs/balanced-match@4.0.1': {}
'@isaacs/brace-expansion@5.0.0':
dependencies:
'@isaacs/balanced-match': 4.0.1
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
string-width-cjs: string-width@4.2.3
strip-ansi: 7.1.2
strip-ansi-cjs: strip-ansi@6.0.1
wrap-ansi: 8.1.0
wrap-ansi-cjs: wrap-ansi@7.0.0
'@jridgewell/gen-mapping@0.3.13': '@jridgewell/gen-mapping@0.3.13':
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
@@ -4760,6 +4928,27 @@ snapshots:
'@polka/url@1.0.0-next.29': {} '@polka/url@1.0.0-next.29': {}
'@posthog/cli@0.5.5':
dependencies:
axios: 1.12.2
axios-proxy-builder: 0.1.2
console.table: 0.10.0
detect-libc: 2.1.2
rimraf: 6.0.1
transitivePeerDependencies:
- debug
'@posthog/core@1.3.1': {}
'@posthog/nextjs-config@1.3.6(next@15.5.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.93.2))':
dependencies:
'@posthog/cli': 0.5.5
'@posthog/core': 1.3.1
next: 15.5.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.93.2)
semver: 7.7.3
transitivePeerDependencies:
- debug
'@radix-ui/colors@3.0.0': {} '@radix-ui/colors@3.0.0': {}
'@radix-ui/number@1.1.1': {} '@radix-ui/number@1.1.1': {}
@@ -6099,12 +6288,26 @@ snapshots:
async-function@1.0.0: {} async-function@1.0.0: {}
asynckit@0.4.0: {}
available-typed-arrays@1.0.7: available-typed-arrays@1.0.7:
dependencies: dependencies:
possible-typed-array-names: 1.1.0 possible-typed-array-names: 1.1.0
axe-core@4.11.0: {} axe-core@4.11.0: {}
axios-proxy-builder@0.1.2:
dependencies:
tunnel: 0.0.6
axios@1.12.2:
dependencies:
follow-redirects: 1.15.11
form-data: 4.0.4
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
axobject-query@4.1.0: {} axobject-query@4.1.0: {}
balanced-match@1.0.2: {} balanced-match@1.0.2: {}
@@ -6177,6 +6380,9 @@ snapshots:
strip-ansi: 6.0.1 strip-ansi: 6.0.1
wrap-ansi: 7.0.0 wrap-ansi: 7.0.0
clone@1.0.4:
optional: true
clsx@2.1.1: {} clsx@2.1.1: {}
color-convert@2.0.1: color-convert@2.0.1:
@@ -6187,6 +6393,10 @@ snapshots:
colorette@2.0.20: {} colorette@2.0.20: {}
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
commander@13.1.0: {} commander@13.1.0: {}
compare-func@2.0.0: compare-func@2.0.0:
@@ -6196,6 +6406,10 @@ snapshots:
concat-map@0.0.1: {} concat-map@0.0.1: {}
console.table@0.10.0:
dependencies:
easy-table: 1.1.0
conventional-changelog-angular@7.0.0: conventional-changelog-angular@7.0.0:
dependencies: dependencies:
compare-func: 2.0.0 compare-func: 2.0.0
@@ -6211,6 +6425,8 @@ snapshots:
meow: 12.1.1 meow: 12.1.1
split2: 4.2.0 split2: 4.2.0
core-js@3.46.0: {}
cosmiconfig-typescript-loader@6.2.0(@types/node@24.9.1)(cosmiconfig@9.0.0(typescript@5.9.3))(typescript@5.9.3): cosmiconfig-typescript-loader@6.2.0(@types/node@24.9.1)(cosmiconfig@9.0.0(typescript@5.9.3))(typescript@5.9.3):
dependencies: dependencies:
'@types/node': 24.9.1 '@types/node': 24.9.1
@@ -6275,6 +6491,11 @@ snapshots:
deep-is@0.1.4: {} deep-is@0.1.4: {}
defaults@1.0.4:
dependencies:
clone: 1.0.4
optional: true
define-data-property@1.1.4: define-data-property@1.1.4:
dependencies: dependencies:
es-define-property: 1.0.1 es-define-property: 1.0.1
@@ -6287,6 +6508,8 @@ snapshots:
has-property-descriptors: 1.0.2 has-property-descriptors: 1.0.2
object-keys: 1.1.1 object-keys: 1.1.1
delayed-stream@1.0.0: {}
deprecation@2.3.1: {} deprecation@2.3.1: {}
dequal@2.0.3: {} dequal@2.0.3: {}
@@ -6316,6 +6539,12 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
gopd: 1.2.0 gopd: 1.2.0
eastasianwidth@0.2.0: {}
easy-table@1.1.0:
optionalDependencies:
wcwidth: 1.0.1
emoji-regex@10.6.0: {} emoji-regex@10.6.0: {}
emoji-regex@8.0.0: {} emoji-regex@8.0.0: {}
@@ -6719,6 +6948,8 @@ snapshots:
optionalDependencies: optionalDependencies:
picomatch: 4.0.3 picomatch: 4.0.3
fflate@0.4.8: {}
fflate@0.8.2: {} fflate@0.8.2: {}
file-entry-cache@8.0.0: file-entry-cache@8.0.0:
@@ -6747,10 +6978,25 @@ snapshots:
flatted@3.3.3: {} flatted@3.3.3: {}
follow-redirects@1.15.11: {}
for-each@0.3.5: for-each@0.3.5:
dependencies: dependencies:
is-callable: 1.2.7 is-callable: 1.2.7
foreground-child@3.3.1:
dependencies:
cross-spawn: 7.0.6
signal-exit: 4.1.0
form-data@4.0.4:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
es-set-tostringtag: 2.1.0
hasown: 2.0.2
mime-types: 2.1.35
fsevents@2.3.3: fsevents@2.3.3:
optional: true optional: true
@@ -6819,6 +7065,15 @@ snapshots:
dependencies: dependencies:
is-glob: 4.0.3 is-glob: 4.0.3
glob@11.0.3:
dependencies:
foreground-child: 3.3.1
jackspeak: 4.1.1
minimatch: 10.0.3
minipass: 7.1.2
package-json-from-dist: 1.0.1
path-scurry: 2.0.0
global-directory@4.0.1: global-directory@4.0.1:
dependencies: dependencies:
ini: 4.1.1 ini: 4.1.1
@@ -7059,6 +7314,10 @@ snapshots:
has-symbols: 1.1.0 has-symbols: 1.1.0
set-function-name: 2.0.2 set-function-name: 2.0.2
jackspeak@4.1.1:
dependencies:
'@isaacs/cliui': 8.0.2
jiti@2.6.1: {} jiti@2.6.1: {}
js-tokens@4.0.0: {} js-tokens@4.0.0: {}
@@ -7224,6 +7483,8 @@ snapshots:
dependencies: dependencies:
js-tokens: 4.0.0 js-tokens: 4.0.0
lru-cache@11.2.2: {}
lz-string@1.5.0: {} lz-string@1.5.0: {}
magic-string@0.30.19: magic-string@0.30.19:
@@ -7253,12 +7514,22 @@ snapshots:
braces: 3.0.3 braces: 3.0.3
picomatch: 2.3.1 picomatch: 2.3.1
mime-db@1.52.0: {}
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
mimic-fn@4.0.0: {} mimic-fn@4.0.0: {}
mimic-function@5.0.1: {} mimic-function@5.0.1: {}
min-indent@1.0.1: {} min-indent@1.0.1: {}
minimatch@10.0.3:
dependencies:
'@isaacs/brace-expansion': 5.0.0
minimatch@3.1.2: minimatch@3.1.2:
dependencies: dependencies:
brace-expansion: 1.1.12 brace-expansion: 1.1.12
@@ -7269,6 +7540,8 @@ snapshots:
minimist@1.2.8: {} minimist@1.2.8: {}
minipass@7.1.2: {}
mrmime@2.0.1: {} mrmime@2.0.1: {}
ms@2.1.3: {} ms@2.1.3: {}
@@ -7407,6 +7680,8 @@ snapshots:
dependencies: dependencies:
p-limit: 4.0.0 p-limit: 4.0.0
package-json-from-dist@1.0.1: {}
parent-module@1.0.1: parent-module@1.0.1:
dependencies: dependencies:
callsites: 3.1.0 callsites: 3.1.0
@@ -7428,6 +7703,11 @@ snapshots:
path-parse@1.0.7: {} path-parse@1.0.7: {}
path-scurry@2.0.0:
dependencies:
lru-cache: 11.2.2
minipass: 7.1.2
pathe@2.0.3: {} pathe@2.0.3: {}
picocolors@1.1.1: {} picocolors@1.1.1: {}
@@ -7452,6 +7732,16 @@ snapshots:
picocolors: 1.1.1 picocolors: 1.1.1
source-map-js: 1.2.1 source-map-js: 1.2.1
posthog-js@1.280.1:
dependencies:
'@posthog/core': 1.3.1
core-js: 3.46.0
fflate: 0.4.8
preact: 10.27.2
web-vitals: 4.2.4
preact@10.27.2: {}
prelude-ls@1.2.1: {} prelude-ls@1.2.1: {}
prettier-plugin-tailwindcss@0.7.1(prettier@3.6.2): prettier-plugin-tailwindcss@0.7.1(prettier@3.6.2):
@@ -7472,6 +7762,8 @@ snapshots:
object-assign: 4.1.1 object-assign: 4.1.1
react-is: 16.13.1 react-is: 16.13.1
proxy-from-env@1.1.0: {}
punycode@2.3.1: {} punycode@2.3.1: {}
queue-microtask@1.2.3: {} queue-microtask@1.2.3: {}
@@ -7643,6 +7935,11 @@ snapshots:
rfdc@1.4.1: {} rfdc@1.4.1: {}
rimraf@6.0.1:
dependencies:
glob: 11.0.3
package-json-from-dist: 1.0.1
rollup@4.52.5: rollup@4.52.5:
dependencies: dependencies:
'@types/estree': 1.0.8 '@types/estree': 1.0.8
@@ -7837,6 +8134,12 @@ snapshots:
is-fullwidth-code-point: 3.0.0 is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1 strip-ansi: 6.0.1
string-width@5.1.2:
dependencies:
eastasianwidth: 0.2.0
emoji-regex: 9.2.2
strip-ansi: 7.1.2
string-width@7.2.0: string-width@7.2.0:
dependencies: dependencies:
emoji-regex: 10.6.0 emoji-regex: 10.6.0
@@ -8139,6 +8442,13 @@ snapshots:
- tsx - tsx
- yaml - yaml
wcwidth@1.0.1:
dependencies:
defaults: 1.0.4
optional: true
web-vitals@4.2.4: {}
webpack-virtual-modules@0.6.2: {} webpack-virtual-modules@0.6.2: {}
whatwg-mimetype@3.0.0: {} whatwg-mimetype@3.0.0: {}
@@ -8201,6 +8511,12 @@ snapshots:
string-width: 4.2.3 string-width: 4.2.3
strip-ansi: 6.0.1 strip-ansi: 6.0.1
wrap-ansi@8.1.0:
dependencies:
ansi-styles: 6.2.3
string-width: 5.1.2
strip-ansi: 7.1.2
wrap-ansi@9.0.2: wrap-ansi@9.0.2:
dependencies: dependencies:
ansi-styles: 6.2.3 ansi-styles: 6.2.3

View File

@@ -3,6 +3,7 @@ import { useState, useCallback, useEffect, useRef } from "react";
import { CheckIcon, ClipboardIcon } from "@radix-ui/react-icons"; import { CheckIcon, ClipboardIcon } from "@radix-ui/react-icons";
import type { IconButtonProps } from "@radix-ui/themes"; import type { IconButtonProps } from "@radix-ui/themes";
import { IconButton, Tooltip } from "@radix-ui/themes"; import { IconButton, Tooltip } from "@radix-ui/themes";
import { useTelemetry } from "@/contexts/TelemetryContext";
/** /**
* Duration in milliseconds for how long the "copied" state persists * Duration in milliseconds for how long the "copied" state persists
@@ -64,6 +65,7 @@ const CopyButton: FunctionComponent<CopyButtonProps> = ({
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const [tooltipOpen, setTooltipOpen] = useState(false); const [tooltipOpen, setTooltipOpen] = useState(false);
const forceOpenRef = useRef(false); const forceOpenRef = useRef(false);
const { track } = useTelemetry();
// Consolidated timer effect: Reset copied state, tooltip, and force-open flag // Consolidated timer effect: Reset copied state, tooltip, and force-open flag
useEffect(() => { useEffect(() => {
@@ -85,12 +87,22 @@ const CopyButton: FunctionComponent<CopyButtonProps> = ({
navigator.clipboard.writeText(value).then( navigator.clipboard.writeText(value).then(
() => { () => {
setCopied(true); setCopied(true);
// Track copy action
track({
name: "user_interaction",
properties: {
action: "copy_button",
component: "CopyButton",
value: value.length, // Track length instead of actual value for privacy
},
});
}, },
(err) => { (err) => {
console.error("Failed to copy to clipboard:", err); console.error("Failed to copy to clipboard:", err);
} }
); );
}, [value]); }, [value, track]);
const handleTooltipOpenChange = useCallback((open: boolean) => { const handleTooltipOpenChange = useCallback((open: boolean) => {
// Don't allow the tooltip to close if we're in the forced-open period // Don't allow the tooltip to close if we're in the forced-open period

View File

@@ -0,0 +1,92 @@
/**
* Error boundary component that catches React errors and tracks them via telemetry.
*/
import type { ReactNode, ErrorInfo } from "react";
import { Component } from "react";
import { Box, Flex, Heading, Text, Button } from "@radix-ui/themes";
import { telemetry } from "@/telemetry/client";
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: (error: Error) => ReactNode;
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
// Track the error via telemetry
telemetry.track({
name: "error",
properties: {
errorType: "runtime_error",
message: error.message,
stack: error.stack,
context: {
componentStack: errorInfo.componentStack,
route: typeof window !== "undefined" ? window.location.pathname : "unknown",
},
},
});
// Log to console for debugging
console.error("ErrorBoundary caught an error:", error, errorInfo);
}
render() {
if (this.state.hasError && this.state.error) {
// If custom fallback is provided, use it
if (this.props.fallback) {
return this.props.fallback(this.state.error);
}
// Default fallback UI using Radix components
return (
<Flex direction="column" align="center" justify="center" p="9" gap="4">
<Box>
<Heading size="6" align="center" mb="2">
Something went wrong
</Heading>
<Text align="center" color="gray">
{this.state.error.message}
</Text>
</Box>
<Flex gap="3">
<Button
variant="soft"
onClick={() => {
this.setState({ hasError: false, error: null });
}}
>
Try Again
</Button>
<Button
onClick={() => {
window.location.reload();
}}
>
Reload Page
</Button>
</Flex>
</Flex>
);
}
return this.props.children;
}
}
export default ErrorBoundary;

View File

@@ -4,6 +4,7 @@ import { useTheme } from "next-themes";
import { IconButton } from "@radix-ui/themes"; import { IconButton } from "@radix-ui/themes";
import { MoonIcon, SunIcon, DesktopIcon } from "@radix-ui/react-icons"; import { MoonIcon, SunIcon, DesktopIcon } from "@radix-ui/react-icons";
import { useEffect, useState, type ReactElement } from "react"; import { useEffect, useState, type ReactElement } from "react";
import { useTelemetry } from "@/contexts/TelemetryContext";
type Theme = "light" | "dark" | "system"; type Theme = "light" | "dark" | "system";
@@ -21,6 +22,7 @@ const isTheme = (value: string | undefined): value is Theme => {
export const ThemeToggle = () => { export const ThemeToggle = () => {
const { theme, setTheme } = useTheme(); const { theme, setTheme } = useTheme();
const { track } = useTelemetry();
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
// Avoid hydration mismatch by only rendering after mount // Avoid hydration mismatch by only rendering after mount
@@ -36,6 +38,16 @@ export const ThemeToggle = () => {
const { icon, next } = THEME_CONFIG[currentTheme]; const { icon, next } = THEME_CONFIG[currentTheme];
const toggleTheme = () => { const toggleTheme = () => {
// Track theme change
track({
name: "user_interaction",
properties: {
action: "theme_toggle",
component: "ThemeToggle",
value: next,
},
});
setTheme(next); setTheme(next);
}; };

View File

@@ -1,5 +1,6 @@
import type { FunctionComponent, ReactNode } from "react"; import type { FunctionComponent, ReactNode } from "react";
import { createContext, useContext, useState, useEffect, useCallback } from "react"; import { createContext, useContext, useState, useEffect, useCallback } from "react";
import { useTelemetry } from "@/telemetry";
type DateFormat = "relative" | "absolute"; type DateFormat = "relative" | "absolute";
@@ -13,6 +14,8 @@ const DateFormatContext = createContext<DateFormatContextType | undefined>(undef
const STORAGE_KEY = "global-date-format-preference"; const STORAGE_KEY = "global-date-format-preference";
export const DateFormatProvider: FunctionComponent<{ children: ReactNode }> = ({ children }) => { export const DateFormatProvider: FunctionComponent<{ children: ReactNode }> = ({ children }) => {
const { track } = useTelemetry();
const [format, setFormat] = useState<DateFormat>(() => { const [format, setFormat] = useState<DateFormat>(() => {
// Initialize from localStorage on client side // Initialize from localStorage on client side
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
@@ -30,8 +33,22 @@ export const DateFormatProvider: FunctionComponent<{ children: ReactNode }> = ({
}, [format]); }, [format]);
const toggleFormat = useCallback(() => { const toggleFormat = useCallback(() => {
setFormat((current) => (current === "relative" ? "absolute" : "relative")); setFormat((current) => {
}, []); const newFormat = current === "relative" ? "absolute" : "relative";
// Track date format change
track({
name: "user_interaction",
properties: {
action: "date_format_change",
component: "DateFormatProvider",
value: newFormat,
},
});
return newFormat;
});
}, [track]);
return ( return (
<DateFormatContext.Provider value={{ format, toggleFormat }}> <DateFormatContext.Provider value={{ format, toggleFormat }}>

View File

@@ -0,0 +1,76 @@
/**
* Telemetry context provider for PostHog integration.
* Provides useTelemetry hook and automatic page view tracking.
*/
import { createContext, useContext, useEffect, type ReactNode } from "react";
import { useRouter } from "next/router";
import { telemetry } from "@/telemetry/client";
import type { TelemetryEvent } from "@/telemetry/events";
interface TelemetryContextValue {
track: <E extends TelemetryEvent>(event: E) => void;
identify: (userId: string, properties?: Record<string, unknown>) => void;
reset: () => void;
isEnabled: () => boolean;
}
const TelemetryContext = createContext<TelemetryContextValue | null>(null);
interface TelemetryProviderProps {
children: ReactNode;
}
export function TelemetryProvider({ children }: TelemetryProviderProps) {
const router = useRouter();
// Initialize telemetry on mount
useEffect(() => {
telemetry.init();
}, []);
// Track page views on route change
useEffect(() => {
const handleRouteChange = (url: string) => {
telemetry.track({
name: "page_view",
properties: {
route: url,
referrer: document.referrer,
},
});
};
// Track initial page view
handleRouteChange(router.asPath);
// Subscribe to route changes
// Note: routeChangeComplete only fires for subsequent navigations, not the initial load,
// so the initial page view tracked above won't be duplicated
router.events.on("routeChangeComplete", handleRouteChange);
return () => {
router.events.off("routeChangeComplete", handleRouteChange);
};
}, [router.asPath, router.events]);
const value: TelemetryContextValue = {
track: telemetry.track.bind(telemetry),
identify: telemetry.identify.bind(telemetry),
reset: telemetry.reset.bind(telemetry),
isEnabled: telemetry.isEnabled.bind(telemetry),
};
return <TelemetryContext.Provider value={value}>{children}</TelemetryContext.Provider>;
}
/**
* Hook to access telemetry client from any component
*/
export function useTelemetry(): TelemetryContextValue {
const context = useContext(TelemetryContext);
if (!context) {
throw new Error("useTelemetry must be used within a TelemetryProvider");
}
return context;
}

7
src/env/schema.mjs vendored
View File

@@ -7,6 +7,8 @@ import { z } from "zod";
*/ */
export const serverSchema = z.object({ export const serverSchema = z.object({
NODE_ENV: z.enum(["development", "test", "production"]), NODE_ENV: z.enum(["development", "test", "production"]),
// PostHog source map upload configuration (optional, only needed for production builds)
POSTHOG_PERSONAL_API_KEY: z.string().optional(),
}); });
/** /**
@@ -16,6 +18,7 @@ export const serverSchema = z.object({
*/ */
export const serverEnv = { export const serverEnv = {
NODE_ENV: process.env.NODE_ENV, NODE_ENV: process.env.NODE_ENV,
POSTHOG_PERSONAL_API_KEY: process.env.POSTHOG_PERSONAL_API_KEY,
}; };
/** /**
@@ -25,6 +28,8 @@ export const serverEnv = {
*/ */
export const clientSchema = z.object({ export const clientSchema = z.object({
// NEXT_PUBLIC_CLIENTVAR: z.string(), // NEXT_PUBLIC_CLIENTVAR: z.string(),
NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(),
NEXT_PUBLIC_POSTHOG_HOST: z.string().optional(),
}); });
/** /**
@@ -35,4 +40,6 @@ export const clientSchema = z.object({
*/ */
export const clientEnv = { export const clientEnv = {
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR, // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST,
}; };

View File

@@ -9,21 +9,27 @@ import "overlayscrollbars/overlayscrollbars.css";
import "@/styles/globals.css"; import "@/styles/globals.css";
import { DateFormatProvider } from "@/contexts/DateFormatContext"; import { DateFormatProvider } from "@/contexts/DateFormatContext";
import { TelemetryProvider } from "@/contexts/TelemetryContext";
import ErrorBoundary from "@/components/ErrorBoundary";
const MyApp: AppType = ({ Component, pageProps }) => { const MyApp: AppType = ({ Component, pageProps }) => {
return ( return (
<ThemeProvider <ErrorBoundary>
attribute="class" <ThemeProvider
defaultTheme="system" attribute="class"
// Cloudflare Rocket Loader breaks the script injection and causes theme flashing defaultTheme="system"
scriptProps={{ "data-cfasync": "false" }} // Cloudflare Rocket Loader breaks the script injection and causes theme flashing
> scriptProps={{ "data-cfasync": "false" }}
<Theme accentColor="indigo" grayColor="slate" radius="medium" scaling="100%"> >
<DateFormatProvider> <Theme accentColor="indigo" grayColor="slate" radius="medium" scaling="100%">
<Component {...pageProps} /> <TelemetryProvider>
</DateFormatProvider> <DateFormatProvider>
</Theme> <Component {...pageProps} />
</ThemeProvider> </DateFormatProvider>
</TelemetryProvider>
</Theme>
</ThemeProvider>
</ErrorBoundary>
); );
}; };

View File

@@ -12,6 +12,7 @@ import {
generateBootstrapWarning, generateBootstrapWarning,
} from "@/rdap/services/type-detection"; } from "@/rdap/services/type-detection";
import { executeRdapQuery, HttpSecurityError } from "@/rdap/services/query"; import { executeRdapQuery, HttpSecurityError } from "@/rdap/services/query";
import { useTelemetry } from "@/contexts/TelemetryContext";
export type WarningHandler = (warning: { message: string }) => void; export type WarningHandler = (warning: { message: string }) => void;
export type UrlUpdateHandler = (target: string, manuallySelectedType: TargetType | null) => void; export type UrlUpdateHandler = (target: string, manuallySelectedType: TargetType | null) => void;
@@ -33,6 +34,8 @@ const useLookup = (warningHandler?: WarningHandler, urlUpdateHandler?: UrlUpdate
// Used to allow repeatable lookups when weird errors happen. // Used to allow repeatable lookups when weird errors happen.
const repeatableRef = useRef<string>(""); const repeatableRef = useRef<string>("");
const { track } = useTelemetry();
const getTypeEasy = useCallback(async (target: string): Promise<Result<TargetType, Error>> => { const getTypeEasy = useCallback(async (target: string): Promise<Result<TargetType, Error>> => {
return detectTargetType(target, getRegistry); return detectTargetType(target, getRegistry);
}, []); }, []);
@@ -112,6 +115,9 @@ const useLookup = (warningHandler?: WarningHandler, urlUpdateHandler?: UrlUpdate
targetType = detectedType.value; targetType = detectedType.value;
} }
// Track query start
const startTime = performance.now();
// Execute the RDAP query using the extracted service // Execute the RDAP query using the extracted service
const result = await executeRdapQuery(target, targetType, { const result = await executeRdapQuery(target, targetType, {
requestJSContact, requestJSContact,
@@ -119,6 +125,56 @@ const useLookup = (warningHandler?: WarningHandler, urlUpdateHandler?: UrlUpdate
repeatableUrl: repeatableRef.current, repeatableUrl: repeatableRef.current,
}); });
// Calculate duration
const duration = performance.now() - startTime;
// Track query result
if (result.isOk) {
track({
name: "rdap_query",
properties: {
targetType,
success: true,
duration,
},
});
} else {
// Determine error type
let errorType = "unknown_error";
if (result.error instanceof HttpSecurityError) {
errorType = "http_security_error";
} else if (result.error.message.includes("network")) {
errorType = "network_error";
} else if (result.error.message.includes("validation")) {
errorType = "validation_error";
}
track({
name: "rdap_query",
properties: {
targetType,
target,
success: false,
errorType,
duration,
},
});
// Also track detailed error
track({
name: "error",
properties: {
errorType: "rdap_query_error",
message: result.error.message,
stack: result.error.stack,
context: {
target,
targetType,
},
},
});
}
// Update repeatable ref if we got an HTTP security error for domain lookups // Update repeatable ref if we got an HTTP security error for domain lookups
if (result.isErr && result.error instanceof HttpSecurityError) { if (result.isErr && result.error instanceof HttpSecurityError) {
repeatableRef.current = result.error.url; repeatableRef.current = result.error.url;

114
src/telemetry/client.ts Normal file
View File

@@ -0,0 +1,114 @@
/**
* Telemetry client wrapper for PostHog with type-safe event tracking.
* Provides console logging in development/CI environments when PostHog keys are not configured.
*/
import posthog from "posthog-js";
import { env } from "@/env/client.mjs";
import type { TelemetryEvent } from "@/telemetry/events";
class TelemetryClient {
private initialized = false;
private enabled = false;
/**
* Centralized logging method that only logs in development or when PostHog is disabled
*/
private log(message: string, data?: unknown): void {
if (process.env.NODE_ENV !== "production" || !this.enabled) {
if (data !== undefined) {
console.log(`[Telemetry] ${message}`, data);
} else {
console.log(`[Telemetry] ${message}`);
}
}
}
/**
* Ensure the client is initialized before use
*/
private ensureInitialized(): void {
if (!this.initialized) {
this.init();
}
}
/**
* Initialize the PostHog client if keys are available
*/
init(): void {
if (this.initialized) return;
// Only enable PostHog if both key and host are configured
if (env.NEXT_PUBLIC_POSTHOG_KEY && env.NEXT_PUBLIC_POSTHOG_HOST) {
posthog.init(env.NEXT_PUBLIC_POSTHOG_KEY, {
api_host: env.NEXT_PUBLIC_POSTHOG_HOST,
loaded: (ph) => {
// Disable in development for debugging purposes
if (process.env.NODE_ENV === "development") {
ph.debug();
}
},
capture_pageview: false, // We'll handle page views manually
capture_pageleave: true,
persistence: "localStorage",
});
this.enabled = true;
this.log("PostHog initialized");
} else {
this.enabled = false;
this.log("PostHog not configured, console logging enabled");
}
this.initialized = true;
}
/**
* Track a telemetry event with type safety
*/
track<E extends TelemetryEvent>(event: E): void {
this.ensureInitialized();
this.log(event.name, event.properties);
if (this.enabled) {
posthog.capture(event.name, event.properties);
}
}
/**
* Identify a user with properties
*/
identify(userId: string, properties?: Record<string, unknown>): void {
this.ensureInitialized();
this.log("identify", { userId, properties });
if (this.enabled) {
posthog.identify(userId, properties);
}
}
/**
* Reset user identification (e.g., on logout)
*/
reset(): void {
if (!this.initialized) return;
this.log("reset");
if (this.enabled) {
posthog.reset();
}
}
/**
* Check if telemetry is enabled
*/
isEnabled(): boolean {
this.ensureInitialized();
return this.enabled;
}
}
/**
* Singleton telemetry client instance
*/
export const telemetry = new TelemetryClient();

91
src/telemetry/events.ts Normal file
View File

@@ -0,0 +1,91 @@
/**
* Type-safe telemetry event system using discriminated unions.
* All events must have a 'name' discriminator property.
*/
import type { TargetType } from "@/rdap/schemas";
/**
* Page view tracking event
*/
export type PageViewEvent = {
name: "page_view";
properties: {
route: string;
referrer?: string;
duration?: number;
};
};
/**
* RDAP query tracking event
*/
export type RdapQueryEvent = {
name: "rdap_query";
properties: {
targetType: TargetType;
target?: string;
success: boolean;
errorType?: string;
duration?: number;
};
};
/**
* User interaction tracking event
*/
export type UserInteractionEvent = {
name: "user_interaction";
properties: {
action:
| "theme_toggle"
| "date_format_change"
| "copy_button"
| "link_click"
| "expand_section"
| "collapse_section"
| string;
component?: string;
value?: string | number | boolean;
};
};
/**
* Error tracking event
*/
export type ErrorEvent = {
name: "error";
properties: {
errorType:
| "rdap_query_error"
| "network_error"
| "validation_error"
| "runtime_error"
| string;
message: string;
stack?: string;
context?: Record<string, unknown>;
};
};
/**
* Discriminated union of all possible events
*/
export type TelemetryEvent = PageViewEvent | RdapQueryEvent | UserInteractionEvent | ErrorEvent;
/**
* Helper type to extract event properties by event name
*/
export type EventProperties<T extends TelemetryEvent["name"]> = Extract<
TelemetryEvent,
{ name: T }
>["properties"];
/**
* Type guard to ensure an event conforms to the TelemetryEvent schema
*/
export function isValidEvent(event: unknown): event is TelemetryEvent {
if (!event || typeof event !== "object") return false;
const e = event as Partial<TelemetryEvent>;
return typeof e.name === "string" && typeof e.properties === "object" && e.properties !== null;
}

14
src/telemetry/index.ts Normal file
View File

@@ -0,0 +1,14 @@
/**
* Telemetry module exports
*/
export { telemetry } from "@/telemetry/client";
export { useTelemetry } from "@/contexts/TelemetryContext";
export type {
TelemetryEvent,
PageViewEvent,
RdapQueryEvent,
UserInteractionEvent,
ErrorEvent,
EventProperties,
} from "@/telemetry/events";