Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 9 additions & 68 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: CI/CD Pipeline
name: CI

on:
push:
Expand All @@ -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 }}
Expand All @@ -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
25 changes: 15 additions & 10 deletions .svelte-kit/ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -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;

}

/**
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
}
}
7 changes: 5 additions & 2 deletions .svelte-kit/generated/client/app.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import * as client_hooks from '../../../src/hooks.client.ts';


export { matchers } from './matchers.js';

export const nodes = [
Expand Down Expand Up @@ -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: {}
};
Expand Down
2 changes: 1 addition & 1 deletion .svelte-kit/generated/client/nodes/1.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions .svelte-kit/generated/server/internal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -25,7 +25,7 @@ export const options = {
app: ({ head, body, assets, nonce, env }) => "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n\t\t<meta name=\"text-scale\" content=\"scale\" />\n\t\t<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n\t\t<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin />\n\t\t<link\n\t\t\thref=\"https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500;700&family=Geist:wght@400;500;600;700;800&display=swap\"\n\t\t\trel=\"stylesheet\"\n\t\t/>\n\t\t" + head + "\n\t</head>\n\t<body data-sveltekit-preload-data=\"hover\">\n\t\t<div style=\"display: contents\">" + body + "</div>\n\t</body>\n</html>\n",
error: ({ status, message }) => "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<title>" + message + "</title>\n\n\t\t<style>\n\t\t\tbody {\n\t\t\t\t--bg: white;\n\t\t\t\t--fg: #222;\n\t\t\t\t--divider: #ccc;\n\t\t\t\tbackground: var(--bg);\n\t\t\t\tcolor: var(--fg);\n\t\t\t\tfont-family:\n\t\t\t\t\tsystem-ui,\n\t\t\t\t\t-apple-system,\n\t\t\t\t\tBlinkMacSystemFont,\n\t\t\t\t\t'Segoe UI',\n\t\t\t\t\tRoboto,\n\t\t\t\t\tOxygen,\n\t\t\t\t\tUbuntu,\n\t\t\t\t\tCantarell,\n\t\t\t\t\t'Open Sans',\n\t\t\t\t\t'Helvetica Neue',\n\t\t\t\t\tsans-serif;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tjustify-content: center;\n\t\t\t\theight: 100vh;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t.error {\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tmax-width: 32rem;\n\t\t\t\tmargin: 0 1rem;\n\t\t\t}\n\n\t\t\t.status {\n\t\t\t\tfont-weight: 200;\n\t\t\t\tfont-size: 3rem;\n\t\t\t\tline-height: 1;\n\t\t\t\tposition: relative;\n\t\t\t\ttop: -0.05rem;\n\t\t\t}\n\n\t\t\t.message {\n\t\t\t\tborder-left: 1px solid var(--divider);\n\t\t\t\tpadding: 0 0 0 1rem;\n\t\t\t\tmargin: 0 0 0 1rem;\n\t\t\t\tmin-height: 2.5rem;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t}\n\n\t\t\t.message h1 {\n\t\t\t\tfont-weight: 400;\n\t\t\t\tfont-size: 1em;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t@media (prefers-color-scheme: dark) {\n\t\t\t\tbody {\n\t\t\t\t\t--bg: #222;\n\t\t\t\t\t--fg: #ddd;\n\t\t\t\t\t--divider: #666;\n\t\t\t\t}\n\t\t\t}\n\t\t</style>\n\t</head>\n\t<body>\n\t\t<div class=\"error\">\n\t\t\t<span class=\"status\">" + status + "</span>\n\t\t\t<div class=\"message\">\n\t\t\t\t<h1>" + message + "</h1>\n\t\t\t</div>\n\t\t</div>\n\t</body>\n</html>\n"
},
version_hash: "t6rkup"
version_hash: "1yejf0e"
};

export async function get_hooks() {
Expand All @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion src/hooks.client.ts
Original file line number Diff line number Diff line change
@@ -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, {
Expand All @@ -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 };
};
32 changes: 15 additions & 17 deletions src/hooks.server.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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(() => {});
}
Comment on lines +51 to +53

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Capturing a pageview on the server here will likely result in double-counting, as the PostHog client-side SDK also captures pageviews by default on the initial load. In a SvelteKit application, it is generally preferred to rely on client-side tracking for richer telemetry (e.g., browser info, screen resolution). If server-side tracking is intentionally desired, you should disable client-side auto-capture by setting capture_pageview: false in the PostHog initialization config.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Added capture_pageview: false to client PostHog init config to prevent double-counting. Server-side pageview tracking is intentionally kept for route-level analytics that client-side auto-capture misses (SSR navigation, bot traffic). hooks.server.ts:51-53.


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(() => {});
});
});
Comment on lines +63 to +67

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether this app targets Node-only runtime or an edge adapter
fd -i "svelte.config.*" --exec rg -n "adapter|kit"
rg -nP "@sveltejs/adapter|adapter-" package.json

Repository: Jonathangadeaharder/Tilt

Length of output: 244


Remove or refactor signal handlers for static adapter compatibility.

This codebase uses @sveltejs/adapter-static, which generates static sites. Module-level signal handlers like process.on(signal) are executed during the build phase, but they won't function in static deployments since there is no running server to catch signals. Either remove these handlers if unnecessary for static builds, or migrate to a Node.js adapter (e.g., @sveltejs/adapter-node) if server-side lifecycle management is required.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks.server.ts` around lines 63 - 67, The module-level process signal
handlers using ['SIGTERM','SIGINT'] and process.on(...) around shutdown() are
incompatible with adapter-static and should be removed or gated; locate the
signal registration block that calls process.on(signal, () => {
shutdown().catch(() => {}); }) and either delete that block entirely or wrap it
behind an environment/runtime check (e.g., only register when using a Node
runtime) so shutdown() is only wired when running with a server adapter
(adapter-node) or a proper server process.

104 changes: 104 additions & 0 deletions src/lib/telemetry.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>) {
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';

Check warning on line 13 in src/lib/telemetry.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'props.userId' will use Object's default stringification format ('[object Object]') when stringified.

See more on https://sonarcloud.io/project/issues?id=Jonathangadeaharder_Poker&issues=AZ4HM2V_Vn59jIeS8gE3&open=AZ4HM2V_Vn59jIeS8gE3&pullRequest=252
ph?.capture({ distinctId, event, properties: props });
Comment on lines +13 to +14

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Harden distinctId derivation to avoid [object Object] identities.

Line 13 currently stringifies any props.userId, so object values collapse to [object Object] and can merge unrelated users in telemetry.

💡 Suggested fix
-export async function track(event: string, props?: Record<string, unknown>) {
+type TrackProps = Record<string, unknown> & { userId?: string | number };
+
+export async function track(event: string, props?: TrackProps) {
@@
-		const distinctId = props?.userId ? String(props.userId) : 'server';
+		const distinctId =
+			typeof props?.userId === 'string' || typeof props?.userId === 'number'
+				? String(props.userId)
+				: 'server';
🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[warning] 13-13: 'props.userId' will use Object's default stringification format ('[object Object]') when stringified.

See more on https://sonarcloud.io/project/issues?id=Jonathangadeaharder_Poker&issues=AZ4HM2V_Vn59jIeS8gE3&open=AZ4HM2V_Vn59jIeS8gE3&pullRequest=252

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/telemetry.ts` around lines 13 - 14, The current distinctId assignment
can turn object userIds into "[object Object]"; change the derivation so it only
accepts primitive userId values (string or number) and falls back to 'server'
for anything else: inspect props?.userId with typeof checks (e.g., === 'string'
or === 'number') before calling String(...) and use that safe distinctId when
calling ph?.capture (the variable currently declared as distinctId and the
capture invocation).

}
}

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<string, unknown>) {
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),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Including the error stack trace provides essential context for debugging server-side issues in PostHog. Currently, only the error message is captured.

Suggested change
error: err instanceof Error ? err.message : String(err),
error: err instanceof Error ? err.message : String(err),
stack: err instanceof Error ? err.stack : undefined,

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Added stack: err instanceof Error ? err.stack : undefined to server_error properties. telemetry.ts:38-39.

stack: err instanceof Error ? err.stack : undefined,
...ctx
}
});
}
}

export async function identify(userId: string, traits?: Record<string, unknown>) {
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<string, any> | 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<string, unknown>).__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<string, unknown>).__tilt_init = true;
}
return clientPhInstance;
Comment on lines +72 to +90

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify explicit any usage and current client instance typing in telemetry wrapper
rg -nP --type=ts '\bany\b' src/lib/telemetry.ts
rg -nP --type=ts 'clientPhInstance|getClientPh\s*\(' src/lib/telemetry.ts

Repository: Jonathangadeaharder/Tilt

Length of output: 721


🏁 Script executed:

cat -n src/lib/telemetry.ts | head -100

Repository: Jonathangadeaharder/Tilt

Length of output: 3639


🏁 Script executed:

# Check if PostHog types are available or need to be inferred
rg -nP --type=ts 'import.*posthog|from.*posthog' src/lib/telemetry.ts

Repository: Jonathangadeaharder/Tilt

Length of output: 237


🏁 Script executed:

# Check how the return value of getClientPh() is used in the codebase
rg -B2 -A3 'await getClientPh\(\)' src/lib/telemetry.ts

Repository: Jonathangadeaharder/Tilt

Length of output: 813


Replace any-based client instance typing with concrete PostHog types.

Record<string, any> on line 72 violates strict typing requirements and leaks any through getClientPh(). This is inconsistent with the server-side client at line 93, which properly uses import('posthog-node').PostHog.

Suggested fix
-let clientPhInstance: Record<string, any> | null = null;
+type PostHogClient = typeof import('posthog-js').default;
+type TiltPostHogClient = PostHogClient & { __tilt_init?: boolean };
+let clientPhInstance: TiltPostHogClient | null = null;
 
-async function getClientPh() {
+async function getClientPh(): Promise<TiltPostHogClient | null> {
@@
-	if (!(clientPhInstance as Record<string, unknown>).__tilt_init) {
+	if (!clientPhInstance.__tilt_init) {
@@
-		(clientPhInstance as Record<string, unknown>).__tilt_init = true;
+		clientPhInstance.__tilt_init = true;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let clientPhInstance: Record<string, any> | 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<string, unknown>).__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<string, unknown>).__tilt_init = true;
}
return clientPhInstance;
type PostHogClient = typeof import('posthog-js').default;
type TiltPostHogClient = PostHogClient & { __tilt_init?: boolean };
let clientPhInstance: TiltPostHogClient | null = null;
async function getClientPh(): Promise<TiltPostHogClient | null> {
if (!browser) return null;
if (clientPhInstance) return clientPhInstance;
const mod = await import('posthog-js');
clientPhInstance = mod.default;
if (!clientPhInstance.__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.__tilt_init = true;
}
return clientPhInstance;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/telemetry.ts` around lines 72 - 90, Replace the loose any typing for
clientPhInstance with a concrete PostHog type and a small local extension for
the injected __tilt_init flag: import the PostHog type from 'posthog-js',
declare an interface (e.g. TiltPostHog extends PostHog { __tilt_init?: boolean
}), change clientPhInstance's type to TiltPostHog | null and update
getClientPh's return type accordingly, and remove or adjust any
(clientPhInstance as Record<string, unknown>) casts to use the new TiltPostHog
type so the client is strictly typed like the server-side posthog-node usage.

}

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;
}
Loading