diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fee5970..b5524c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: CI/CD Pipeline +name: CI on: push: @@ -7,8 +7,8 @@ on: branches: [main, develop] jobs: - lint-and-test: - name: Lint and Test + ci: + name: CI runs-on: self-hosted env: PUBLIC_POSTHOG_PROJECT_TOKEN: ${{ vars.PUBLIC_POSTHOG_PROJECT_TOKEN }} @@ -32,77 +32,18 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Run linter + - name: Lint run: pnpm run lint - - name: Run typecheck + - name: Typecheck run: pnpm run typecheck - - name: Run tests + - name: Test run: pnpm run test - security: - name: Security Audit - runs-on: self-hosted - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - version: 10 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Run audit - run: pnpm audit --audit-level=moderate - continue-on-error: true - - code-quality: - name: Code Quality - runs-on: self-hosted - env: - PUBLIC_POSTHOG_PROJECT_TOKEN: ${{ vars.PUBLIC_POSTHOG_PROJECT_TOKEN }} - PUBLIC_POSTHOG_HOST: ${{ vars.PUBLIC_POSTHOG_HOST }} - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - version: 10 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - name: Build run: pnpm run build - notify: - name: Notify - runs-on: self-hosted - needs: [lint-and-test, security, code-quality] - if: always() - - steps: - - name: Check status - run: | - echo "Pipeline status: ${{ job.status }}" - echo "Build completed" + - name: Audit + run: pnpm audit --audit-level=moderate + continue-on-error: true diff --git a/.svelte-kit/ambient.d.ts b/.svelte-kit/ambient.d.ts index 4e24073..dcc1835 100644 --- a/.svelte-kit/ambient.d.ts +++ b/.svelte-kit/ambient.d.ts @@ -81,7 +81,6 @@ declare module '$env/static/private' { export const OPENCODE_PROCESS_ROLE: string; export const SHLVL: string; export const HOME: string; - export const CI: string; export const HOMEBREW_PREFIX: string; export const FNM_DIR: string; export const LOGNAME: string; @@ -98,6 +97,13 @@ declare module '$env/static/private' { export const OPENCODE: string; export const COLORTERM: string; export const npm_node_execpath: string; + export const TEST: string; + export const VITEST: string; + export const NODE_ENV: string; + export const PROD: string; + export const DEV: string; + export const BASE_URL: string; + export const MODE: string; } /** @@ -134,10 +140,7 @@ declare module '$env/static/private' { * The above values will be the same _even if_ different values for `ENVIRONMENT` or `PUBLIC_BASE_URL` are set at runtime, as they are statically replaced in your code with their build time values. */ declare module '$env/static/public' { - export const PUBLIC_SUPABASE_ANON_KEY: string; - export const PUBLIC_SUPABASE_URL: string; - export const PUBLIC_POSTHOG_HOST: string; - export const PUBLIC_POSTHOG_PROJECT_TOKEN: string; + } /** @@ -230,7 +233,6 @@ declare module '$env/dynamic/private' { OPENCODE_PROCESS_ROLE: string; SHLVL: string; HOME: string; - CI: string; HOMEBREW_PREFIX: string; FNM_DIR: string; LOGNAME: string; @@ -247,6 +249,13 @@ declare module '$env/dynamic/private' { OPENCODE: string; COLORTERM: string; npm_node_execpath: string; + TEST: string; + VITEST: string; + NODE_ENV: string; + PROD: string; + DEV: string; + BASE_URL: string; + MODE: string; [key: `PUBLIC_${string}`]: undefined; [key: `${string}`]: string | undefined; } @@ -302,10 +311,6 @@ declare module '$env/dynamic/private' { */ declare module '$env/dynamic/public' { export const env: { - PUBLIC_SUPABASE_ANON_KEY: string; - PUBLIC_SUPABASE_URL: string; - PUBLIC_POSTHOG_HOST: string; - PUBLIC_POSTHOG_PROJECT_TOKEN: string; [key: `PUBLIC_${string}`]: string | undefined; } } diff --git a/.svelte-kit/generated/client/app.js b/.svelte-kit/generated/client/app.js index 83211c7..4f02e6d 100644 --- a/.svelte-kit/generated/client/app.js +++ b/.svelte-kit/generated/client/app.js @@ -1,3 +1,6 @@ +import * as client_hooks from '../../../src/hooks.client.ts'; + + export { matchers } from './matchers.js'; export const nodes = [ @@ -40,8 +43,8 @@ export const dictionary = { }; export const hooks = { - handleError: (({ error }) => { console.error(error) }), - + handleError: client_hooks.handleError || (({ error }) => { console.error(error) }), + init: client_hooks.init, reroute: (() => {}), transport: {} }; diff --git a/.svelte-kit/generated/client/nodes/1.js b/.svelte-kit/generated/client/nodes/1.js index 453cc99..7b65978 100644 --- a/.svelte-kit/generated/client/nodes/1.js +++ b/.svelte-kit/generated/client/nodes/1.js @@ -1 +1 @@ -export { default as component } from "../../../../node_modules/.pnpm/@sveltejs+kit@2.59.0_@sveltejs+vite-plugin-svelte@7.0.0_svelte@5.55.5_vite@8.0.10_@type_72ff1850efe0a9b28484ed03a4d12607/node_modules/@sveltejs/kit/src/runtime/components/svelte-5/error.svelte"; \ No newline at end of file +export { default as component } from "../../../../node_modules/.pnpm/@sveltejs+kit@2.59.0_@opentelemetry+api@1.9.1_@sveltejs+vite-plugin-svelte@7.0.0_svelte_ffcaade9fdd02ff53867e36bd9218907/node_modules/@sveltejs/kit/src/runtime/components/svelte-5/error.svelte"; \ No newline at end of file diff --git a/.svelte-kit/generated/server/internal.js b/.svelte-kit/generated/server/internal.js index 560f4ba..717b6f2 100644 --- a/.svelte-kit/generated/server/internal.js +++ b/.svelte-kit/generated/server/internal.js @@ -3,7 +3,7 @@ import root from '../root.js'; import { set_building, set_prerendering } from '__sveltekit/environment'; import { set_assets } from '$app/paths/internal/server'; import { set_manifest, set_read_implementation } from '__sveltekit/server'; -import { set_private_env, set_public_env } from '../../../node_modules/.pnpm/@sveltejs+kit@2.59.0_@sveltejs+vite-plugin-svelte@7.0.0_svelte@5.55.5_vite@8.0.10_@type_72ff1850efe0a9b28484ed03a4d12607/node_modules/@sveltejs/kit/src/runtime/shared-server.js'; +import { set_private_env, set_public_env } from '../../../node_modules/.pnpm/@sveltejs+kit@2.59.0_@opentelemetry+api@1.9.1_@sveltejs+vite-plugin-svelte@7.0.0_svelte_ffcaade9fdd02ff53867e36bd9218907/node_modules/@sveltejs/kit/src/runtime/shared-server.js'; export const options = { app_template_contains_nonce: false, @@ -25,7 +25,7 @@ export const options = { app: ({ head, body, assets, nonce, env }) => "\n\n\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t" + head + "\n\t\n\t\n\t\t
" + body + "
\n\t\n\n", error: ({ status, message }) => "\n\n\t\n\t\t\n\t\t" + message + "\n\n\t\t\n\t\n\t\n\t\t
\n\t\t\t" + status + "\n\t\t\t
\n\t\t\t\t

" + message + "

\n\t\t\t
\n\t\t
\n\t\n\n" }, - version_hash: "t6rkup" + version_hash: "1yejf0e" }; export async function get_hooks() { @@ -34,7 +34,7 @@ export async function get_hooks() { let handleError; let handleValidationError; let init; - + ({ handle, handleFetch, handleError, handleValidationError, init } = await import("../../../src/hooks.server.ts")); let reroute; let transport; diff --git a/src/hooks.client.ts b/src/hooks.client.ts index 70c4c21..2d6c025 100644 --- a/src/hooks.client.ts +++ b/src/hooks.client.ts @@ -1,6 +1,7 @@ import type { HandleClientError } from '@sveltejs/kit'; import posthog from 'posthog-js'; import { PUBLIC_POSTHOG_HOST, PUBLIC_POSTHOG_PROJECT_TOKEN } from '$env/static/public'; +import { captureError } from '$lib/telemetry'; export async function init() { posthog.init(PUBLIC_POSTHOG_PROJECT_TOKEN, { @@ -13,6 +14,6 @@ export async function init() { } export const handleError: HandleClientError = async ({ error, status, message }) => { - posthog.captureException(error); + captureError(error, { status, message, source: 'client' }).catch(() => {}); return { message, status }; }; diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 024f0ee..e2fb0d6 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,6 +1,6 @@ import type { Handle, HandleServerError } from '@sveltejs/kit'; import { PUBLIC_POSTHOG_HOST } from '$env/static/public'; -import { getPostHogClient } from '$lib/server/posthog'; +import { captureError, pageview, shutdown } from '$lib/telemetry'; export const handle: Handle = async ({ event, resolve }) => { const { pathname } = event.url; @@ -46,24 +46,22 @@ export const handle: Handle = async ({ event, resolve }) => { } } - return resolve(event); -}; + const response = await resolve(event); -export const handleError: HandleServerError = async ({ error, status, message, event }) => { - try { - const posthog = getPostHogClient(); - const distinctId = (event.locals as any)?.session?.user?.id ?? 'server'; + if (!pathname.startsWith('/api') && !pathname.startsWith('/_')) { + pageview(pathname).catch(() => {}); + } - posthog.capture({ - distinctId, - event: 'server_error', - properties: { - error: error instanceof Error ? error.message : String(error), - status, - message - } - }); - } catch {} + return response; +}; +export const handleError: HandleServerError = async ({ error, status, message }) => { + captureError(error, { status, message, source: 'server' }).catch(() => {}); return { message, status }; }; + +['SIGTERM', 'SIGINT'].forEach((signal) => { + process.on(signal, () => { + shutdown().catch(() => {}); + }); +}); diff --git a/src/lib/telemetry.ts b/src/lib/telemetry.ts new file mode 100644 index 0000000..9bb98d8 --- /dev/null +++ b/src/lib/telemetry.ts @@ -0,0 +1,104 @@ +import { browser } from '$app/environment'; +import { PUBLIC_POSTHOG_HOST, PUBLIC_POSTHOG_PROJECT_TOKEN } from '$env/static/public'; + +const enabled = !!PUBLIC_POSTHOG_PROJECT_TOKEN; + +export async function track(event: string, props?: Record) { + if (!enabled) return; + if (browser) { + const ph = await getClientPh(); + ph?.capture(event, props); + } else { + const ph = await getServerPh(); + const distinctId = props?.userId ? String(props.userId) : 'server'; + ph?.capture({ distinctId, event, properties: props }); + } +} + +export async function pageview(route: string) { + if (!enabled) return; + if (browser) { + const ph = await getClientPh(); + ph?.capture('$pageview', { $current_url: route }); + } else { + const ph = await getServerPh(); + ph?.capture({ distinctId: 'server', event: '$pageview', properties: { $current_url: route } }); + } +} + +export async function captureError(err: unknown, ctx?: Record) { + if (!enabled) return; + if (browser) { + const ph = await getClientPh(); + ph?.captureException(err, ctx); + } else { + const ph = await getServerPh(); + ph?.capture({ + distinctId: 'server', + event: 'server_error', + properties: { + error: err instanceof Error ? err.message : String(err), + stack: err instanceof Error ? err.stack : undefined, + ...ctx + } + }); + } +} + +export async function identify(userId: string, traits?: Record) { + if (!enabled) return; + if (browser) { + const ph = await getClientPh(); + ph?.identify(userId, traits); + } else { + const ph = await getServerPh(); + ph?.identify({ distinctId: userId, properties: traits }); + } +} + +export async function reset() { + if (!enabled || !browser) return; + const ph = await getClientPh(); + ph?.reset(); +} + +export async function shutdown() { + if (!enabled || browser) return; + if (!serverPhInstance) return; + await serverPhInstance.shutdown(); + serverPhInstance = null; +} + +let clientPhInstance: Record | null = null; + +async function getClientPh() { + if (!browser) return null; + if (clientPhInstance) return clientPhInstance; + const mod = await import('posthog-js'); + clientPhInstance = mod.default; + if (!(clientPhInstance as Record).__tilt_init) { + clientPhInstance.init(PUBLIC_POSTHOG_PROJECT_TOKEN, { + api_host: '/ingest', + ui_host: PUBLIC_POSTHOG_HOST, + defaults: '2026-01-30', + capture_pageview: false, + capture_exceptions: true, + person_profiles: 'identified_only' + }); + (clientPhInstance as Record).__tilt_init = true; + } + return clientPhInstance; +} + +let serverPhInstance: import('posthog-node').PostHog | null = null; + +async function getServerPh() { + if (browser) return null; + if (serverPhInstance) return serverPhInstance; + const { PostHog } = await import('posthog-node'); + serverPhInstance = new PostHog(PUBLIC_POSTHOG_PROJECT_TOKEN, { + host: PUBLIC_POSTHOG_HOST, + personProfiles: 'identified_only' + }); + return serverPhInstance; +}