-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add telemetry abstraction #252
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3d287ab
580d4e6
5fda5fc
7b827fb
e013cbd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| 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; | ||
|
|
@@ -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(() => {}); | ||
| }); | ||
| }); | ||
|
Comment on lines
+63
to
+67
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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.jsonRepository: Jonathangadeaharder/Tilt Length of output: 244 Remove or refactor signal handlers for static adapter compatibility. This codebase uses 🤖 Prompt for AI Agents |
||
| 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
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ph?.capture({ distinctId, event, properties: props }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+13
to
+14
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Harden Line 13 currently stringifies any 💡 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. 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Including the error stack trace provides essential context for debugging server-side issues in PostHog. Currently, only the error message is captured.
Suggested change
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed. Added |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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.tsRepository: Jonathangadeaharder/Tilt Length of output: 721 🏁 Script executed: cat -n src/lib/telemetry.ts | head -100Repository: 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.tsRepository: 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.tsRepository: Jonathangadeaharder/Tilt Length of output: 813 Replace
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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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: falsein the PostHog initialization config.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed. Added
capture_pageview: falseto 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.