diff --git a/apps/dashboard/.env.example b/apps/dashboard/.env.example index 8e231c9..d05fd96 100644 --- a/apps/dashboard/.env.example +++ b/apps/dashboard/.env.example @@ -5,6 +5,20 @@ CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE="postgres://postgres:my # The canonical URL. ORIGIN="http://localhost:5173" +# Analytics & error tracking +# PostHog browser SDK. Requests are reverse-proxied through this app at /internal/phog_in so +# ad blockers don't drop them. PUBLIC_POSTHOG_HOST picks the region (us/eu) and is +# also used as ui_host for the toolbar. Leave the key empty to disable PostHog. +PUBLIC_POSTHOG_KEY="" +PUBLIC_POSTHOG_HOST="https://us.posthog.com" +# Sentry DSN (browser + worker). Browser events are tunneled through /internal/sentry_in. +# Leave empty to disable Sentry. +PUBLIC_SENTRY_DSN="" +# Only needed to upload source maps at build time. +SENTRY_ORG="" +SENTRY_PROJECT="" +SENTRY_AUTH_TOKEN="" + # Better Auth # For production use 32+ characters generated with high entropy # https://www.better-auth.com/docs/installation diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 7d508d4..6fa8562 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -76,10 +76,13 @@ "@better-svelte-email/components": "^2.1.1", "@better-svelte-email/server": "^2.1.1", "@plausible-analytics/tracker": "^0.4.5", + "@sentry/sveltekit": "10.75.0", "arktype": "^2.2.3", "autumn-js": "1.2.33", "ip-address": "^10.2.0", "ky": "^2.0.2", + "posthog-js": "1.434.2", + "posthog-node": "^5.52.4", "qrcode": "^1.5.4", "ulid": "^3.0.2", "undici": "^8.9.0", diff --git a/apps/dashboard/src/app.d.ts b/apps/dashboard/src/app.d.ts index 9878d8a..7740391 100644 --- a/apps/dashboard/src/app.d.ts +++ b/apps/dashboard/src/app.d.ts @@ -24,6 +24,9 @@ declare global { ctx: ExecutionContext; env: { ORIGIN: string; + PUBLIC_POSTHOG_KEY?: string; + PUBLIC_POSTHOG_HOST?: string; + PUBLIC_SENTRY_DSN?: string; BETTER_AUTH_SECRET: string; VYOS_API_URL?: string; VYOS_API_KEY?: string; diff --git a/apps/dashboard/src/hooks.client.ts b/apps/dashboard/src/hooks.client.ts new file mode 100644 index 0000000..5c0924e --- /dev/null +++ b/apps/dashboard/src/hooks.client.ts @@ -0,0 +1,21 @@ +import { handleErrorWithSentry } from '@sentry/sveltekit'; +import * as Sentry from '@sentry/sveltekit'; +import type { HandleClientError } from '@sveltejs/kit'; +import { dev } from '$app/environment'; +import { env } from '$env/dynamic/public'; +import { captureClientException } from '$lib/analytics/posthog'; + +if (env.PUBLIC_SENTRY_DSN) { + Sentry.init({ + dsn: env.PUBLIC_SENTRY_DSN, + tunnel: '/internal/sentry_in', + environment: dev ? 'development' : 'production', + sendDefaultPii: false + }); +} + +const forwardToPostHog: HandleClientError = ({ error }) => { + captureClientException(error); +}; + +export const handleError = handleErrorWithSentry(forwardToPostHog); diff --git a/apps/dashboard/src/hooks.server.ts b/apps/dashboard/src/hooks.server.ts index 44041f9..46ef490 100644 --- a/apps/dashboard/src/hooks.server.ts +++ b/apps/dashboard/src/hooks.server.ts @@ -1,5 +1,10 @@ -import { redirect, type Handle } from '@sveltejs/kit'; -import { building } from '$app/environment'; +import { redirect, type Handle, type HandleServerError } from '@sveltejs/kit'; +import { sequence } from '@sveltejs/kit/hooks'; +import { handleErrorWithSentry, initCloudflareSentryHandle, sentryHandle } from '@sentry/sveltekit'; +import { building, dev } from '$app/environment'; +import { env as publicEnv } from '$env/dynamic/public'; +import { handlePostHogProxy } from '$lib/server/posthog-proxy'; +import { captureServerException } from '$lib/server/posthog'; import { getCachedAuthSession, hasAuthSessionCookie } from '$lib/server/auth-lite'; import { closeRequestDb } from '$lib/server/db'; import { instrument, timingLog } from '$lib/server/observability'; @@ -18,6 +23,7 @@ const publicRoutes = [ '/reset-password', '/accept-invitation', '/api/', + '/internal/', '/_app/remote/' ]; const authPages = ['/login', '/register', '/signup', '/forgot-password']; @@ -163,4 +169,29 @@ const handleBetterAuth: Handle = async ({ event, resolve }) => { } }; -export const handle: Handle = handleBetterAuth; +let sentryRequestHandle: Handle | undefined; + +const handleSentryInit: Handle = (input) => { + if (!publicEnv.PUBLIC_SENTRY_DSN) return input.resolve(input.event); + + sentryRequestHandle ??= initCloudflareSentryHandle({ + dsn: publicEnv.PUBLIC_SENTRY_DSN, + environment: dev ? 'development' : 'production', + sendDefaultPii: false + }); + return sentryRequestHandle(input); +}; + +export const handle: Handle = sequence( + handleSentryInit, + sentryHandle({ injectFetchProxyScript: false }), + handlePostHogProxy, + handleBetterAuth +); + +const logServerError: HandleServerError = ({ error, event }) => { + console.error('Unhandled server error', { pathname: event.url.pathname, error }); + captureServerException(error, event); +}; + +export const handleError = handleErrorWithSentry(logServerError); diff --git a/apps/dashboard/src/lib/analytics/posthog.ts b/apps/dashboard/src/lib/analytics/posthog.ts new file mode 100644 index 0000000..f79ca04 --- /dev/null +++ b/apps/dashboard/src/lib/analytics/posthog.ts @@ -0,0 +1,38 @@ +import { browser, dev } from '$app/environment'; +import { env } from '$env/dynamic/public'; +import posthog from 'posthog-js'; + +export const posthogProxyPath = '/internal/phog_in'; + +let initialized = false; + +export function initPostHog() { + if (!browser || initialized) return; + + const token = env.PUBLIC_POSTHOG_KEY; + if (!token) return; + + initialized = true; + posthog.init(token, { + api_host: posthogProxyPath, + ui_host: env.PUBLIC_POSTHOG_HOST || 'https://us.posthog.com', + capture_pageview: 'history_change', + capture_pageleave: 'if_capture_pageview', + capture_exceptions: { + capture_unhandled_errors: true, + capture_unhandled_rejections: true, + capture_console_errors: true + }, + person_profiles: 'identified_only', + logs: { + captureConsoleLogs: true, + serviceName: 'stack-dashboard', + environment: dev ? 'development' : 'production' + } + }); +} + +export function captureClientException(error: unknown) { + if (!initialized) return; + posthog.captureException(error); +} diff --git a/apps/dashboard/src/lib/remote/admin-projects.remote.ts b/apps/dashboard/src/lib/remote/admin-projects.remote.ts index 3bb3f2b..8926269 100644 --- a/apps/dashboard/src/lib/remote/admin-projects.remote.ts +++ b/apps/dashboard/src/lib/remote/admin-projects.remote.ts @@ -33,6 +33,7 @@ import { accessibilityFixtureEnabled, accessibilityFixtureAdminProjects } from '$lib/server/accessibility-fixtures'; +import { captureServerEvent } from '$lib/server/posthog'; export type AdminProjectBillingStatus = 'configured' | 'past_due' | 'suspended' | 'none'; @@ -276,6 +277,12 @@ export const setProjectDisabled = command(setDisabledParams, async (params) => { .set({ disabled: params.disabled }) .where(eq(organization.id, params.projectId)); + captureServerEvent( + 'admin_project_disabled_changed', + { disabled: params.disabled }, + { projectId: params.projectId } + ); + return { projectId: params.projectId, disabled: params.disabled }; }); @@ -322,6 +329,11 @@ export const deleteProjectWithVerification = command(deleteProjectParams, async await consumeAdminVerification(db, adminUser.id, params.projectId, params.method, params.code); await softDeleteOrganizationResources(db, params.projectId); + captureServerEvent( + 'admin_project_deleted', + { verification_method: params.method }, + { projectId: params.projectId } + ); return { projectId: params.projectId, name: target.name }; }); diff --git a/apps/dashboard/src/lib/remote/admin-users.remote.ts b/apps/dashboard/src/lib/remote/admin-users.remote.ts index 0317f51..586b3ce 100644 --- a/apps/dashboard/src/lib/remote/admin-users.remote.ts +++ b/apps/dashboard/src/lib/remote/admin-users.remote.ts @@ -29,6 +29,7 @@ import { accessibilityFixtureEnabled, accessibilityFixtureAdminUsers } from '$lib/server/accessibility-fixtures'; +import { captureServerEvent } from '$lib/server/posthog'; export type UserSession = { id: string; @@ -249,6 +250,11 @@ export const setUserDisabled = command(setDisabledParams, async (params) => { .set({ banned: params.disabled, banReason: params.disabled ? null : target.banReason }) .where(eq(user.id, params.userId)); + captureServerEvent('admin_user_disabled_changed', { + target_user_id: params.userId, + disabled: params.disabled + }); + return { userId: params.userId, disabled: params.disabled }; }); @@ -263,6 +269,11 @@ export const setUserBillingExempt = command(setBillingExemptParams, async (param .set({ billingExempt: params.billingExempt }) .where(eq(user.id, params.userId)); + captureServerEvent('admin_user_billing_exempt_changed', { + target_user_id: params.userId, + billing_exempt: params.billingExempt + }); + return { userId: params.userId, billingExempt: params.billingExempt }; }); @@ -291,6 +302,11 @@ export const setUserRole = command(setRoleParams, async (params) => { .set({ role: params.role, isAdmin: hasAdminRole(params.role) }) .where(eq(user.id, params.userId)); + captureServerEvent('admin_user_role_changed', { + target_user_id: params.userId, + role: params.role + }); + return { userId: params.userId, role: params.role, isAdmin: hasAdminRole(params.role) }; }); @@ -327,6 +343,10 @@ export const deleteUserWithVerification = command(deleteUserParams, async (param await consumeAdminVerification(db, adminUserId, params.userId, params.method, params.code); await deleteUserData(db, params.userId); + captureServerEvent('admin_user_deleted', { + target_user_id: params.userId, + verification_method: params.method + }); return { userId: params.userId, email: target.email }; }); diff --git a/apps/dashboard/src/lib/remote/admin-vms.remote.ts b/apps/dashboard/src/lib/remote/admin-vms.remote.ts index 4a4ce5a..87fcf7d 100644 --- a/apps/dashboard/src/lib/remote/admin-vms.remote.ts +++ b/apps/dashboard/src/lib/remote/admin-vms.remote.ts @@ -12,6 +12,7 @@ import { accessibilityFixtureEnabled, accessibilityFixtureAdminVms } from '$lib/server/accessibility-fixtures'; +import { captureServerEvent } from '$lib/server/posthog'; export type AdminVm = { id: string; @@ -161,6 +162,11 @@ async function adminPowerAction( if (row.status === 'deleting') error(409, `VM "${row.name}" is being deleted`); await getBackend(row.backend)[action](row.id, row.proxmoxId ?? undefined); + captureServerEvent( + 'admin_vm_power_action', + { vm_id: row.id, action }, + { projectId: row.ownerProjectId } + ); } export const adminStartVm = command(powerParams, async (p) => adminPowerAction(p.vmId, 'startVm')); @@ -178,4 +184,5 @@ export const adminDeleteVm = command(powerParams, async (params) => { if (!row.active) return; await queueVmDeletion(db, row); + captureServerEvent('admin_vm_deleted', { vm_id: row.id }, { projectId: row.ownerProjectId }); }); diff --git a/apps/dashboard/src/lib/remote/api-tokens.remote.ts b/apps/dashboard/src/lib/remote/api-tokens.remote.ts index b4d1461..9dbf328 100644 --- a/apps/dashboard/src/lib/remote/api-tokens.remote.ts +++ b/apps/dashboard/src/lib/remote/api-tokens.remote.ts @@ -4,6 +4,7 @@ import { type } from 'arktype'; import { eq, and } from 'drizzle-orm'; import { initDrizzle } from '$lib/server/db'; import { apiTokens } from '$lib/server/db/schema'; +import { captureServerEvent } from '$lib/server/posthog'; type ListResult = { id: string; @@ -64,6 +65,8 @@ export const createApiToken = command(createParams, async (params) => { }) .returning(); + captureServerEvent('api_token_created', { token_id: inserted.id }); + return { id: inserted.id, token: plainToken }; }); @@ -83,4 +86,5 @@ export const revokeApiToken = command(revokeParams, async (params) => { await db .delete(apiTokens) .where(and(eq(apiTokens.id, params.tokenId), eq(apiTokens.userId, event.locals.user.id))); + captureServerEvent('api_token_revoked', { token_id: params.tokenId }); }); diff --git a/apps/dashboard/src/lib/remote/billing.remote.ts b/apps/dashboard/src/lib/remote/billing.remote.ts index d47163a..9fd2b83 100644 --- a/apps/dashboard/src/lib/remote/billing.remote.ts +++ b/apps/dashboard/src/lib/remote/billing.remote.ts @@ -15,6 +15,7 @@ import { accessibilityFixtureEnabled, accessibilityFixtureBillingOverview } from '$lib/server/accessibility-fixtures'; +import { captureServerEvent } from '$lib/server/posthog'; const projectParams = type({ projectId: 'string' }); const setupParams = type({ projectId: 'string', returnTo: 'string?', discountCode: 'string?' }); @@ -52,6 +53,7 @@ export const openBillingPortal = command(projectParams, async (params) => { params.projectId, `${event.url.origin}/projects/${params.projectId}/billing` ); + captureServerEvent('billing_portal_opened', {}, { projectId: params.projectId }); return { url }; }); @@ -70,6 +72,11 @@ export const purchaseCredits = command(purchaseCreditsParams, async (params) => await requireProjectAccess(db, event.locals.user.id, params.projectId, 'owner'); const url = await purchaseProjectCredits(params.projectId, params.credits); + captureServerEvent( + 'credit_purchase_started', + { credits: params.credits }, + { projectId: params.projectId } + ); return { url }; }); @@ -89,6 +96,11 @@ export const setupProjectBillingPayment = command(setupParams, async (params) => const promoParam = discountCode ? `&billing_promo=${encodeURIComponent(discountCode)}` : ''; const successUrl = `${event.url.origin}${returnPath}${separator}billing_setup=complete${promoParam}`; const url = await setupProjectPayment(params.projectId, successUrl); + captureServerEvent( + 'billing_setup_started', + { has_discount_code: Boolean(discountCode) }, + { projectId: params.projectId } + ); return { url }; }); diff --git a/apps/dashboard/src/lib/remote/email-change.remote.ts b/apps/dashboard/src/lib/remote/email-change.remote.ts index fcf80d6..379c20e 100644 --- a/apps/dashboard/src/lib/remote/email-change.remote.ts +++ b/apps/dashboard/src/lib/remote/email-change.remote.ts @@ -6,6 +6,7 @@ import { initAuth } from '$lib/server/auth'; import { initDrizzle } from '$lib/server/db'; import { user, verification } from '$lib/server/db/schema'; import { ulid } from '$lib/server/id'; +import { captureServerEvent } from '$lib/server/posthog'; const EMAIL_CHANGE_TTL_MS = 60 * 60 * 1000; @@ -68,5 +69,7 @@ export const requestEmailChange = command(emailChangeParams, async (params) => { expiresAt: new Date(Date.now() + EMAIL_CHANGE_TTL_MS) }); + captureServerEvent('email_change_requested'); + return { email: newEmail }; }); diff --git a/apps/dashboard/src/lib/remote/networking.remote.ts b/apps/dashboard/src/lib/remote/networking.remote.ts index 462d317..515f06f 100644 --- a/apps/dashboard/src/lib/remote/networking.remote.ts +++ b/apps/dashboard/src/lib/remote/networking.remote.ts @@ -8,6 +8,7 @@ import { requireProjectAccess } from '$lib/server/auth-context'; import { isBunnyConfigured } from '$lib/server/bunny'; import { setPtrRecord } from '$lib/server/ptr-records'; import type { PermissionLevel } from '$lib/auth/organization-permissions'; +import { captureServerEvent } from '$lib/server/posthog'; async function requireVmAccess(vmId: string, level?: PermissionLevel) { const event = getRequestEvent(); @@ -75,5 +76,12 @@ export const setVmPtrRecord = command(setPtrParams, async (params) => { if (!address) error(400, 'An IP address inside the subnet is required'); const { ipamPrefix, ...rest } = allocation; - return setPtrRecord(db, { ...rest, sourcePrefix: ipamPrefix }, address, params.value); + const result = await setPtrRecord( + db, + { ...rest, sourcePrefix: ipamPrefix }, + address, + params.value + ); + captureServerEvent('vm_ptr_record_set', { vm_id: params.vmId }); + return result; }); diff --git a/apps/dashboard/src/lib/remote/password-change.remote.ts b/apps/dashboard/src/lib/remote/password-change.remote.ts index 1e59607..2df926a 100644 --- a/apps/dashboard/src/lib/remote/password-change.remote.ts +++ b/apps/dashboard/src/lib/remote/password-change.remote.ts @@ -9,6 +9,7 @@ import { verification } from '$lib/server/db/schema'; import { sendRenderedEmail } from '$lib/server/email'; import { ulid } from '$lib/server/id'; import { account } from '$lib/server/db/auth.schema'; +import { captureServerEvent } from '$lib/server/posthog'; const CODE_LENGTH = 6; const CODE_TTL_MS = 10 * 60 * 1000; @@ -136,6 +137,7 @@ export const confirmPasswordChangeWithEmail = command(emailParams, async (params await changePasswordOauth(params.newPassword); } await db.delete(verification).where(eq(verification.id, record.id)); + captureServerEvent('password_changed', { verification_method: 'email' }); }); const totpParams = type({ currentPassword: 'string', newPassword: 'string', code: 'string' }); @@ -159,6 +161,7 @@ export const confirmPasswordChangeWithTotp = command(totpParams, async (params) } else { await changePasswordOauth(params.newPassword); } + captureServerEvent('password_changed', { verification_method: 'totp' }); }); const passkeyParams = type({ currentPassword: 'string', newPassword: 'string' }); @@ -184,4 +187,5 @@ export const confirmPasswordChangeWithPasskey = command(passkeyParams, async (pa await changePasswordOauth(params.newPassword); } await db.delete(verification).where(eq(verification.id, record.id)); + captureServerEvent('password_changed', { verification_method: 'passkey' }); }); diff --git a/apps/dashboard/src/lib/remote/projects.remote.ts b/apps/dashboard/src/lib/remote/projects.remote.ts index 9da506b..a65f518 100644 --- a/apps/dashboard/src/lib/remote/projects.remote.ts +++ b/apps/dashboard/src/lib/remote/projects.remote.ts @@ -18,6 +18,7 @@ import { accessibilityFixtureProjects } from '$lib/server/accessibility-fixtures'; import { softDeleteOrganizationResources } from '$lib/server/project-deletion'; +import { captureServerEvent } from '$lib/server/posthog'; type ListResult = { id: string; @@ -203,6 +204,7 @@ export const createProject = command(createParams, async (params) => { console.warn(`Failed to sync Autumn customer for project ${org.id}`, err); }); clearProjectListCache(event.locals.user.id); + captureServerEvent('project_created', {}, { projectId: org.id }); return { id: org.id }; }); @@ -217,6 +219,7 @@ export const deleteProject = command(deleteParams, async (params) => { await softDeleteOrganizationResources(db, params.projectId); clearProjectListCache(); + captureServerEvent('project_deleted', {}, { projectId: params.projectId }); }); const updateParams = type({ projectId: 'string', name: 'string' }); @@ -236,6 +239,7 @@ export const updateProject = command(updateParams, async (params) => { console.warn(`Failed to sync Autumn customer update for project ${params.projectId}`, err); }); clearProjectListCache(); + captureServerEvent('project_renamed', {}, { projectId: params.projectId }); }); const addMemberParams = type({ @@ -260,6 +264,11 @@ export const addMember = command(addMemberParams, async (params) => { resend: true } }); + captureServerEvent( + 'project_member_invited', + { permissions: params.permissions }, + { projectId: params.projectId } + ); }); const updateMemberRoleParams = type({ @@ -279,6 +288,11 @@ export const updateMemberRole = command(updateMemberRoleParams, async (params) = .set({ role: params.permissions }) .where(and(eq(member.organizationId, params.projectId), eq(member.userId, params.userId))); clearProjectListCache(); + captureServerEvent( + 'project_member_role_updated', + { member_user_id: params.userId, permissions: params.permissions }, + { projectId: params.projectId } + ); }); const removeMemberParams = type({ projectId: 'string', userId: 'string' }); @@ -297,4 +311,9 @@ export const removeMember = command(removeMemberParams, async (params) => { await db.delete(member).where(eq(member.id, target.id)); clearProjectListCache(); + captureServerEvent( + 'project_member_removed', + { member_user_id: params.userId, previous_role: target.role }, + { projectId: params.projectId } + ); }); diff --git a/apps/dashboard/src/lib/remote/ssh-keys.remote.ts b/apps/dashboard/src/lib/remote/ssh-keys.remote.ts index 3ce85c8..2d6b596 100644 --- a/apps/dashboard/src/lib/remote/ssh-keys.remote.ts +++ b/apps/dashboard/src/lib/remote/ssh-keys.remote.ts @@ -4,6 +4,7 @@ import { type } from 'arktype'; import { eq, and } from 'drizzle-orm'; import { initDrizzle } from '$lib/server/db'; import { sshKeys } from '$lib/server/db/schema'; +import { captureServerEvent } from '$lib/server/posthog'; type ListResult = { id: string; @@ -67,6 +68,8 @@ export const createSshKey = command(createParams, async (params) => { }) .returning(); + captureServerEvent('ssh_key_created', { key_type: parts[0] }); + return { id: inserted.id, fingerprint: inserted.fingerprint }; }); @@ -86,4 +89,5 @@ export const deleteSshKey = command(deleteParams, async (params) => { await db .delete(sshKeys) .where(and(eq(sshKeys.id, params.keyId), eq(sshKeys.userId, event.locals.user.id))); + captureServerEvent('ssh_key_deleted'); }); diff --git a/apps/dashboard/src/lib/remote/two-factor.remote.ts b/apps/dashboard/src/lib/remote/two-factor.remote.ts index 5aa7cb1..d4286df 100644 --- a/apps/dashboard/src/lib/remote/two-factor.remote.ts +++ b/apps/dashboard/src/lib/remote/two-factor.remote.ts @@ -18,6 +18,7 @@ import { verifyTotpResetCode as verifyTotpResetEmailCode, type TotpResetUser } from '$lib/server/totp-reset'; +import { captureServerEvent } from '$lib/server/posthog'; const CODE_LENGTH = 6; @@ -67,6 +68,7 @@ export const disableTwoFactorWithVerification = command(disableTwoFactorParams, message: 'Authenticator app two-factor authentication was disabled for your Stack account.', actionUrl: event.url.origin }); + captureServerEvent('two_factor_disabled', { verification_method: params.method }); }); async function resolveTotpResetUser( @@ -137,5 +139,6 @@ export const confirmTotpResetChoice = command(confirmTotpResetParams, async (par : 'Authenticator app two-factor authentication was disabled for your Stack account during sign-in.', actionUrl: event.url.origin }); + captureServerEvent('totp_reset_completed', { choice }, { distinctId: user.id }); return { choice, signInEmail: user.email }; }); diff --git a/apps/dashboard/src/lib/remote/vms.remote.ts b/apps/dashboard/src/lib/remote/vms.remote.ts index 50ebeba..2aa5761 100644 --- a/apps/dashboard/src/lib/remote/vms.remote.ts +++ b/apps/dashboard/src/lib/remote/vms.remote.ts @@ -32,6 +32,7 @@ import { accessibilityFixtureEnabled, accessibilityFixtureServers } from '$lib/server/accessibility-fixtures'; +import { captureServerEvent } from '$lib/server/posthog'; type VmRow = { id: string; @@ -551,7 +552,7 @@ export const createVm = command(createParams, async (params) => { ? keys.filter((key) => params.sshKeyIds!.includes(key.id)).map((key) => key.publicKey) : []; - return provisionVm(db, { + const created = await provisionVm(db, { projectId: params.projectId, vmTypeId: params.vmTypeId, name: params.name, @@ -562,6 +563,18 @@ export const createVm = command(createParams, async (params) => { sshPublicKeys: publicKeys, password: params.password }); + captureServerEvent( + 'vm_created', + { + vm_type_id: params.vmTypeId, + networking_mode: params.networkingMode ?? 'both', + image_id: params.imageId, + ssh_key_count: publicKeys.length, + billing_exempt: billingExempt + }, + { projectId: params.projectId } + ); + return created; }); const renameParams = type({ vmId: 'string', name: 'string', updateHostname: 'boolean' }); @@ -590,6 +603,11 @@ export const renameVm = command(renameParams, async (params) => { } await db.update(vms).set({ name }).where(eq(vms.id, row.id)); + captureServerEvent( + 'vm_renamed', + { vm_id: row.id, hostname_updated: params.updateHostname }, + { projectId: row.ownerProjectId } + ); return { id: row.id, name }; }); @@ -607,6 +625,11 @@ export const deleteVm = command(deleteParams, async (params) => { } await queueVmDeletion(db, row); + captureServerEvent( + 'vm_deleted', + { vm_id: row.id, vm_type_id: row.vmTypeId }, + { projectId: row.ownerProjectId } + ); }); const powerParams = type({ vmId: 'string' }); @@ -627,6 +650,11 @@ async function powerAction(vmId: string, action: 'startVm' | 'stopVm' | 'killVm' const backend = getBackend(row.backend); await backend[action](row.id, row.proxmoxId ?? undefined); + captureServerEvent( + 'vm_power_action', + { vm_id: row.id, action }, + { projectId: row.ownerProjectId } + ); } export const startVm = command(powerParams, async (p) => powerAction(p.vmId, 'startVm')); @@ -678,6 +706,11 @@ export const resizeVm = command(resizeParams, async (params) => { } await db.update(vms).set({ vmTypeId: target.id }).where(eq(vms.id, row.id)); + captureServerEvent( + 'vm_resized', + { vm_id: row.id, from_vm_type_id: row.vmTypeId, to_vm_type_id: target.id }, + { projectId: row.ownerProjectId } + ); return { id: row.id, vmTypeId: target.id }; }); diff --git a/apps/dashboard/src/lib/remote/volumes.remote.ts b/apps/dashboard/src/lib/remote/volumes.remote.ts index bc30a65..e362581 100644 --- a/apps/dashboard/src/lib/remote/volumes.remote.ts +++ b/apps/dashboard/src/lib/remote/volumes.remote.ts @@ -5,6 +5,7 @@ import { eq } from 'drizzle-orm'; import { initDrizzle } from '$lib/server/db'; import { volumes, vms } from '$lib/server/db/schema'; import { requireProjectAccess } from '$lib/server/auth-context'; +import { captureServerEvent } from '$lib/server/posthog'; type ListParams = { projectId: string }; type ListResult = { @@ -49,6 +50,11 @@ export const createVolume = command(createParams, async (params) => { createdAt: Date.now() }) .returning(); + captureServerEvent( + 'volume_created', + { volume_id: inserted.id, size: params.size, backend: params.backend }, + { projectId: params.projectId } + ); return { id: inserted.id }; }); @@ -71,6 +77,11 @@ export const deleteVolume = command(deleteParams, async (params) => { } await db.delete(volumes).where(eq(volumes.id, params.volumeId)); + captureServerEvent( + 'volume_deleted', + { volume_id: vol.id, size: vol.size }, + { projectId: vol.ownerProjectId } + ); }); const attachParams = type({ volumeId: 'string', vmId: 'string' }); @@ -100,6 +111,11 @@ export const attachVolume = command(attachParams, async (params) => { .update(volumes) .set({ associatedVmId: params.vmId }) .where(eq(volumes.id, params.volumeId)); + captureServerEvent( + 'volume_attached', + { volume_id: vol.id, vm_id: params.vmId }, + { projectId: vol.ownerProjectId } + ); }); const detachParams = type({ volumeId: 'string' }); @@ -116,4 +132,9 @@ export const detachVolume = command(detachParams, async (params) => { await requireProjectAccess(db, event.locals.user.id, vol.ownerProjectId, 'read_write'); await db.update(volumes).set({ associatedVmId: null }).where(eq(volumes.id, params.volumeId)); + captureServerEvent( + 'volume_detached', + { volume_id: vol.id, vm_id: vol.associatedVmId }, + { projectId: vol.ownerProjectId } + ); }); diff --git a/apps/dashboard/src/lib/server/auth.ts b/apps/dashboard/src/lib/server/auth.ts index 3dcd5f6..b41358f 100644 --- a/apps/dashboard/src/lib/server/auth.ts +++ b/apps/dashboard/src/lib/server/auth.ts @@ -18,6 +18,7 @@ import { sendRenderedEmail } from '$lib/server/email'; import { sendSecurityAlertEmail } from '$lib/server/email-notifications'; import { getRuntimeEnv } from '$lib/server/env'; import { ulid } from '$lib/server/id'; +import { captureServerEvent } from '$lib/server/posthog'; const PENDING_PASSKEY_COOKIE = 'pending_passkey_2fa'; const PENDING_PASSKEY_HINT_COOKIE = 'pending_passkey_2fa_hint'; @@ -165,6 +166,23 @@ function buildAuth() { return { data: { ...newUser, role: isFirstUser ? 'admin' : 'user', isAdmin: isFirstUser } }; + }, + after: async (createdUser) => { + captureServerEvent( + 'user_signed_up', + {}, + { + distinctId: createdUser.id, + set: { email: createdUser.email, name: createdUser.name } + } + ); + } + } + }, + session: { + create: { + after: async (createdSession) => { + captureServerEvent('user_signed_in', {}, { distinctId: createdSession.userId }); } } } diff --git a/apps/dashboard/src/lib/server/posthog-proxy.ts b/apps/dashboard/src/lib/server/posthog-proxy.ts new file mode 100644 index 0000000..68aba22 --- /dev/null +++ b/apps/dashboard/src/lib/server/posthog-proxy.ts @@ -0,0 +1,56 @@ +import type { Handle } from '@sveltejs/kit'; +import { env } from '$env/dynamic/public'; +import { posthogProxyPath } from '$lib/analytics/posthog'; + +function posthogRegion(): 'us' | 'eu' { + return env.PUBLIC_POSTHOG_HOST?.includes('eu.') ? 'eu' : 'us'; +} + +function upstreamHost(pathname: string): string { + const region = posthogRegion(); + const isStaticAsset = + pathname.startsWith(`${posthogProxyPath}/static/`) || + pathname.startsWith(`${posthogProxyPath}/array/`); + return isStaticAsset ? `${region}-assets.i.posthog.com` : `${region}.i.posthog.com`; +} + +function stripHopByHopHeaders(headers: Headers): Headers { + const copy = new Headers(headers); + copy.delete('content-encoding'); + copy.delete('content-length'); + copy.delete('transfer-encoding'); + return copy; +} + +export const handlePostHogProxy: Handle = async ({ event, resolve }) => { + const { pathname, search } = event.url; + if (pathname !== posthogProxyPath && !pathname.startsWith(`${posthogProxyPath}/`)) { + return resolve(event); + } + + const hostname = upstreamHost(pathname); + const upstreamUrl = `https://${hostname}${pathname.slice(posthogProxyPath.length) || '/'}${search}`; + + const headers = new Headers(event.request.headers); + headers.set('host', hostname); + headers.delete('cookie'); + headers.delete('authorization'); + headers.delete('accept-encoding'); + + const clientIp = event.request.headers.get('cf-connecting-ip') ?? event.getClientAddress(); + if (clientIp) headers.set('x-forwarded-for', clientIp); + + const hasBody = event.request.method !== 'GET' && event.request.method !== 'HEAD'; + const upstream = await fetch(upstreamUrl, { + method: event.request.method, + headers, + body: hasBody ? await event.request.arrayBuffer() : undefined, + redirect: 'manual' + }); + + return new Response(upstream.body, { + status: upstream.status, + statusText: upstream.statusText, + headers: stripHopByHopHeaders(upstream.headers) + }); +}; diff --git a/apps/dashboard/src/lib/server/posthog.ts b/apps/dashboard/src/lib/server/posthog.ts new file mode 100644 index 0000000..7f897fa --- /dev/null +++ b/apps/dashboard/src/lib/server/posthog.ts @@ -0,0 +1,90 @@ +import type { RequestEvent } from '@sveltejs/kit'; +import { getRequestEvent } from '$app/server'; +import { env } from '$env/dynamic/public'; +import { PostHog } from 'posthog-node/edge'; + +type EventProperties = Record; + +type CaptureOptions = { + distinctId?: string; + projectId?: string | null; + set?: EventProperties; +}; + +function ingestHost(): string { + return env.PUBLIC_POSTHOG_HOST?.includes('eu.') + ? 'https://eu.i.posthog.com' + : 'https://us.i.posthog.com'; +} + +function createClient(): PostHog | null { + const token = env.PUBLIC_POSTHOG_KEY; + if (!token) return null; + return new PostHog(token, { host: ingestHost(), flushAt: 1, flushInterval: 0 }); +} + +function currentEvent(): RequestEvent | null { + try { + return getRequestEvent(); + } catch { + return null; + } +} + +function dispatch(event: RequestEvent | null, send: (client: PostHog) => Promise) { + const client = createClient(); + if (!client) return; + + const pending = send(client) + .then(() => client._shutdown()) + .catch((captureError) => console.warn('PostHog capture failed', captureError)); + + const ctx = event?.platform?.ctx; + if (ctx) ctx.waitUntil(pending); +} + +function requestContext(event: RequestEvent | null): EventProperties { + if (!event) return {}; + return { + $current_url: event.url.href, + route: event.route.id, + method: event.request.method + }; +} + +export function captureServerEvent( + name: string, + properties: EventProperties = {}, + options: CaptureOptions = {} +) { + const event = currentEvent(); + const distinctId = options.distinctId ?? event?.locals.user?.id; + if (!distinctId) return; + + const projectId = options.projectId ?? event?.locals.activeProjectId ?? undefined; + + dispatch(event, async (client) => { + client.capture({ + distinctId, + event: name, + properties: { + ...requestContext(event), + ...properties, + ...(projectId ? { project_id: projectId } : {}), + ...(options.set ? { $set: options.set } : {}) + }, + groups: projectId ? { project: projectId } : undefined + }); + await client.flush(); + }); +} + +export function captureServerException(error: unknown, event: RequestEvent) { + const distinctId = event.locals.user?.id ?? 'anonymous-server'; + dispatch(event, (client) => + client.captureExceptionImmediate(error, distinctId, { + ...requestContext(event), + project_id: event.locals.activeProjectId ?? undefined + }) + ); +} diff --git a/apps/dashboard/src/routes/+layout.svelte b/apps/dashboard/src/routes/+layout.svelte index 3fcfe98..b9c441d 100644 --- a/apps/dashboard/src/routes/+layout.svelte +++ b/apps/dashboard/src/routes/+layout.svelte @@ -3,10 +3,12 @@ import NavigationProgress from '$lib/components/navigation-progress.svelte'; import { onMount } from 'svelte'; import { ModeWatcher } from 'mode-watcher'; + import { initPostHog } from '$lib/analytics/posthog'; let { children } = $props(); onMount(async () => { + initPostHog(); const { init } = await import('@plausible-analytics/tracker'); init({ domain: 'dash.fyrastack.com', diff --git a/apps/dashboard/src/routes/internal/sentry_in/+server.ts b/apps/dashboard/src/routes/internal/sentry_in/+server.ts new file mode 100644 index 0000000..f28a3bb --- /dev/null +++ b/apps/dashboard/src/routes/internal/sentry_in/+server.ts @@ -0,0 +1,41 @@ +import { error, json, type RequestHandler } from '@sveltejs/kit'; +import { env } from '$env/dynamic/public'; + +function allowedProject(): { host: string; projectId: string } | null { + const dsn = env.PUBLIC_SENTRY_DSN; + if (!dsn) return null; + const url = new URL(dsn); + return { host: url.hostname, projectId: url.pathname.replace(/^\//, '') }; +} + +export const POST: RequestHandler = async ({ request, fetch }) => { + const allowed = allowedProject(); + if (!allowed) throw error(404, 'Sentry is not configured'); + + const envelope = await request.arrayBuffer(); + const headerLine = new TextDecoder().decode(envelope).split('\n')[0]; + + let dsn: URL; + try { + dsn = new URL(JSON.parse(headerLine).dsn); + } catch { + throw error(400, 'Invalid Sentry envelope'); + } + + const projectId = dsn.pathname.replace(/^\//, ''); + if (dsn.hostname !== allowed.host || projectId !== allowed.projectId) { + throw error(403, 'Envelope does not match the configured Sentry project'); + } + + const upstream = await fetch(`https://${allowed.host}/api/${projectId}/envelope/`, { + method: 'POST', + headers: { 'content-type': 'application/x-sentry-envelope' }, + body: envelope + }); + + if (!upstream.ok) { + throw error(502, `Sentry rejected the envelope (${upstream.status})`); + } + + return json({}); +}; diff --git a/apps/dashboard/vite.config.ts b/apps/dashboard/vite.config.ts index 6e19849..4fb8756 100644 --- a/apps/dashboard/vite.config.ts +++ b/apps/dashboard/vite.config.ts @@ -1,5 +1,6 @@ import tailwindcss from '@tailwindcss/vite'; import { sveltekit } from '@sveltejs/kit/vite'; +import { sentrySvelteKit } from '@sentry/sveltekit'; import { defineConfig, loadEnv } from 'vite'; import Icons from 'unplugin-icons/vite'; import { FileSystemIconLoader } from 'unplugin-icons/loaders'; @@ -23,6 +24,13 @@ export default defineConfig(({ mode }) => { }, plugins: [ tailwindcss(), + sentrySvelteKit({ + autoInstrument: false, + autoUploadSourceMaps: Boolean(env.SENTRY_AUTH_TOKEN), + org: env.SENTRY_ORG, + project: env.SENTRY_PROJECT, + authToken: env.SENTRY_AUTH_TOKEN + }), sveltekit(), Icons({ compiler: 'svelte', diff --git a/apps/dashboard/wrangler.jsonc b/apps/dashboard/wrangler.jsonc index 4a4807d..dd246e8 100644 --- a/apps/dashboard/wrangler.jsonc +++ b/apps/dashboard/wrangler.jsonc @@ -14,6 +14,9 @@ ], "vars": { "ORIGIN": "https://dash.fyrastack.com", + "PUBLIC_POSTHOG_KEY": "phc_xpPsXXeqca5GUtozk5DvUdTFMwAjYN7z4SDYLHSqwHpX", + "PUBLIC_POSTHOG_HOST": "https://us.posthog.com", + "PUBLIC_SENTRY_DSN": "https://509fc8298796e65c16acbef704256178@o271654.ingest.us.sentry.io/4512122021412864", "GITHUB_CLIENT_ID": "Ov23liHWucfwO0M7A6mb", "EMAIL_FROM_ADDRESS": "no-reply@fyrastack.com", "EMAIL_FROM_NAME": "Fyra Stack", @@ -38,6 +41,7 @@ "run_worker_first": [ "/", "/_app/remote/*", + "/internal/*", "/api/*", "/admin/*", "/projects/*", @@ -109,6 +113,9 @@ "minify": true, "vars": { "ORIGIN": "https://stack-dashboard-preview.fyralabs.workers.dev", + "PUBLIC_POSTHOG_KEY": "phc_xpPsXXeqca5GUtozk5DvUdTFMwAjYN7z4SDYLHSqwHpX", + "PUBLIC_POSTHOG_HOST": "https://us.posthog.com", + "PUBLIC_SENTRY_DSN": "https://509fc8298796e65c16acbef704256178@o271654.ingest.us.sentry.io/4512122021412864", "EMAIL_FROM_ADDRESS": "no-reply@fyrastack.com", "EMAIL_FROM_NAME": "Fyra Stack", "EMAIL_REPLY_TO": "support@fyralabs.com", diff --git a/apps/dashboard/wrangler.local.jsonc b/apps/dashboard/wrangler.local.jsonc index d8e37ef..3de6ff8 100644 --- a/apps/dashboard/wrangler.local.jsonc +++ b/apps/dashboard/wrangler.local.jsonc @@ -10,6 +10,7 @@ "run_worker_first": [ "/", "/_app/remote/*", + "/internal/*", "/api/*", "/admin/*", "/projects/*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7653edb..d9d7370 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,6 +48,9 @@ importers: '@plausible-analytics/tracker': specifier: ^0.4.5 version: 0.4.5 + '@sentry/sveltekit': + specifier: 10.75.0 + version: 10.75.0(@cloudflare/workers-types@5.20260717.1)(@sveltejs/kit@2.69.3(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.6)(vite@8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.6)(typescript@6.0.3)(vite@8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.6)(vite@8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.9.0))(wrangler@4.111.0(@cloudflare/workers-types@5.20260717.1)) arktype: specifier: ^2.2.3 version: 2.2.3 @@ -60,6 +63,12 @@ importers: ky: specifier: ^2.0.2 version: 2.0.2 + posthog-js: + specifier: 1.434.2 + version: 1.434.2 + posthog-node: + specifier: ^5.52.4 + version: 5.52.4 qrcode: specifier: ^1.5.4 version: 1.5.4 @@ -1148,6 +1157,44 @@ packages: resolution: {integrity: sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==} engines: {node: '>= 20.19.0'} + '@opentelemetry/api-logs@0.220.0': + resolution: {integrity: sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/core@2.11.0': + resolution: {integrity: sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/instrumentation@0.220.0': + resolution: {integrity: sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/resources@2.11.0': + resolution: {integrity: sha512-Ie7+8q8MDF4FAEQCKVMTx3ReUvxiIAgIiiW3c9JdmP8+HMcDy20puT+AHjexnExgnbvBxjQ9fjkFDWrikJ2jQA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.11.0': + resolution: {integrity: sha512-H19x/TX/LZdqiYOjM7fqtSxwlplC5pgelavqbQdHbhdq0q/AI/TGkM2dfGuuynTXmJPeF2HoZVoPDu+TGoW78A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace@2.11.0': + resolution: {integrity: sha512-fFnTqGm8/G73GQVnxYi7LXa1ZVYEUvgL6XI1LpvV0bPC7WQ/ZGgKxCSl8FnlZBKto9JHHEFTO6s6CUpvvtwFrA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/semantic-conventions@1.43.0': resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==} engines: {node: '>=14'} @@ -1215,6 +1262,15 @@ packages: '@poppinss/exception@1.2.3': resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + '@posthog/browser-common@0.9.0': + resolution: {integrity: sha512-EWoVzs1vVY7ihpn8yOjj3jyjriHUEEMVSBNL/TW3hk09fTriWRe76vJXrRg0G0yjTYxHmtlSkNn4mOV24ZEzzA==} + + '@posthog/core@1.55.0': + resolution: {integrity: sha512-x+0FR+uHsK3uJeofd5X4S2YS7hjaWkzkPN0X1GROHm9SvKw2RJ91kfQ7gT1bPtFwZSHNiRtteB7fX/xEMRN0IQ==} + + '@posthog/types@1.412.2': + resolution: {integrity: sha512-taxTh8o6n7ZVlC8qBypZvF+6w7/18yNkS9XDiGCbutGTKqIljfzYi/7pumgwN3mcnl/n0e9A+JhF05+nLd4Xaw==} + '@prisma/client@5.22.0': resolution: {integrity: sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==} engines: {node: '>=16.13'} @@ -1510,6 +1566,167 @@ packages: peerDependencies: selderee: ~0.12.0 + '@sentry/browser-utils@10.75.0': + resolution: {integrity: sha512-yZfdQ87RLat/7retF8zA6Z9zCAT12IyaPvIiu+bbi1E1V1OqDpS8MKNa/6LF69Ap7DVTORi9ITEgjlAqTXF0xQ==} + engines: {node: '>=18'} + + '@sentry/browser@10.75.0': + resolution: {integrity: sha512-CKGSJLWISQ6LgMaGnMpY+XvwWQ2iiHTayUs6SGLIvYeaGo2GTjS/Z4pnbCVHb2t1VxLZksyehgcC6ZYGJeQpzQ==} + engines: {node: '>=18'} + + '@sentry/bundler-plugins@10.75.0': + resolution: {integrity: sha512-kpMO7NhvfqsobpShyW/YnNJmJpw7mPEAQorJnEzf0ZRqNvEGFjKKN19SZWbbBNdcQDvHPlRIVOC00zdXuw3y/w==} + engines: {node: '>= 18'} + peerDependencies: + rollup: '>=3.2.0' + webpack: '>=5.0.0' + peerDependenciesMeta: + rollup: + optional: true + webpack: + optional: true + + '@sentry/cli-darwin@2.58.6': + resolution: {integrity: sha512-udAVvcyfNa0R+95GvPz/+43/N3TC0TYKdkQ7D7jhPSzbcMc7l2fxRNN5yB3UpCA5fWFnW4toeaqwDBhb/Wh3LA==} + engines: {node: '>=10'} + os: [darwin] + + '@sentry/cli-linux-arm64@2.58.6': + resolution: {integrity: sha512-q8mEcNNmeXMy5i+jWT30TVpH7LcP4HD21CD5XRSPAd/a912HF6EpK0ybf/1USO14WOhoXbAGi9txwaWabSe33g==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux, freebsd, android] + + '@sentry/cli-linux-arm@2.58.6': + resolution: {integrity: sha512-pD0LAt5PcUzAinBwvDqc66x9+2CabHEv486yP0gRjWO7SakbaxmfVq/EXd8VLq/Tzi39LAu422UYK1lpW3MILw==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux, freebsd, android] + + '@sentry/cli-linux-i686@2.58.6': + resolution: {integrity: sha512-q8vNJi1eOV/4vxAFWBsEwLHoSYapaZHIf4j76KJGJXFKTkEbsjCOOsKbwUIBTQQhRgV4DFWh3ryfsPS/que4Kg==} + engines: {node: '>=10'} + cpu: [x86, ia32] + os: [linux, freebsd, android] + + '@sentry/cli-linux-x64@2.58.6': + resolution: {integrity: sha512-DZu956Mhi3ZRjTBe1WdbGV46ldVbA8d2rgp/fh51GsI25zjBHah4wZnPTSzpc+YqxU6pJpg579B/r3jrIK530Q==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux, freebsd, android] + + '@sentry/cli-win32-arm64@2.58.6': + resolution: {integrity: sha512-nj0Ff/kmAB73EPDhR8B4O9r+NUHK5GkPCkGWC+kXVemqAJWL5jcJ5KdxG0l/S0z6RoEoltID8/43/B+TaMlT7A==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@sentry/cli-win32-i686@2.58.6': + resolution: {integrity: sha512-WNZiDzPbgsEMQWq4avsQ391v/xWKJDIWWWo9GYl+N/w5qcYKkoDW7wQG7T9FasI6ENn68phChTOAPXXxbfAdOg==} + engines: {node: '>=10'} + cpu: [x86, ia32] + os: [win32] + + '@sentry/cli-win32-x64@2.58.6': + resolution: {integrity: sha512-R35WJ17oF4D2eqI1DR2sQQqr0fjRTt5xoP16WrTu91XM2lndRMFsnjh+/GttbxapLCBNlrjzia99MJ0PZHZpgA==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@sentry/cli@2.58.6': + resolution: {integrity: sha512-baBcNPLLfUi9WuL+Tpri9BFaAdvugZIKelC5X0tt0Zdy+K0K+PCVSrnNmwMWU/HyaF/SEv6b6UHnXIdqanBlcg==} + engines: {node: '>= 10'} + hasBin: true + + '@sentry/cloudflare@10.75.0': + resolution: {integrity: sha512-2TvEXTihK8tNT0KB7WraoiSsSJ9dX6h4F7Wr0+k5rldPH9G1l6zc+6xP/u9CCB+m44XOHiI2omPGDkQ9uEo1ew==} + engines: {node: '>=18'} + peerDependencies: + '@cloudflare/workers-types': ^4.x || ^5.x + wrangler: ^4.x + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + wrangler: + optional: true + + '@sentry/conventions@0.16.0': + resolution: {integrity: sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ==} + engines: {node: '>=14'} + + '@sentry/core@10.75.0': + resolution: {integrity: sha512-5wDQpQqjJ6RHdPR+z6Q+47XlwoxRKBv0yyB0LKHqL/1azPOQbR+nllyLmmQDcoJpaksuLSmLA1q6hFX7gjDT2w==} + engines: {node: '>=18'} + + '@sentry/feedback@10.75.0': + resolution: {integrity: sha512-YLVAFQXELJAOg7ke4oLkd8t5l0fJUockLlTQJg/2Trh7SB5Q4mkSwlnAkEAqDQ618vnpFsG/tnYMwtfZeffHjA==} + engines: {node: '>=18'} + + '@sentry/node-core@10.75.0': + resolution: {integrity: sha512-+E3KSX1oMqhpWQ23hj6NblIVUzLy3T2oceU7DfMX1s1ar+npaQZxR5K0/cMGcQgS1LLm8Jqwqo/lrZlKTCDm5A==} + engines: {node: '>=18'} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/core': ^1.30.1 || ^2.1.0 + '@opentelemetry/exporter-trace-otlp-http': '>=0.57.0 <1' + '@opentelemetry/instrumentation': '>=0.57.1 <1' + '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@opentelemetry/core': + optional: true + '@opentelemetry/exporter-trace-otlp-http': + optional: true + '@opentelemetry/instrumentation': + optional: true + '@opentelemetry/sdk-trace-base': + optional: true + + '@sentry/node@10.75.0': + resolution: {integrity: sha512-XdYW+SEiscQnuugh1hMldfnHurmleKSTSDqgfro6Y1yd7s0r2csnZ5/SEYxIeL4lSl1QXg1lIA1pBmAXjcdnKA==} + engines: {node: '>=18'} + + '@sentry/opentelemetry@10.75.0': + resolution: {integrity: sha512-reJoMtuHMuaiztoDVoaeitc22eHlTAvz2qpFhibWyzukJlF8oXB+o8EvDN9mmDDMedU89c//cXRcnPBlJvUxcw==} + engines: {node: '>=18'} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/core': ^1.30.1 || ^2.1.0 + '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 + + '@sentry/replay-canvas@10.75.0': + resolution: {integrity: sha512-LNokI5Ud22nZ/o2il/2SfbNIARupWrbS6Mh9jZroZTjD38FN0Oi9vIexj6h1HN81C6+lP2Tmpaj+fl3/wOizzA==} + engines: {node: '>=18'} + + '@sentry/replay@10.75.0': + resolution: {integrity: sha512-p4xxEo/ayiMZG9tygr09YjqYAd52uq8Qi0EVoRRgUe70nQda0HVPVW519JLq0G+Th/5O5sKME9y1layLU3xlPg==} + engines: {node: '>=18'} + + '@sentry/server-utils@10.75.0': + resolution: {integrity: sha512-7fIa9vFGNzB13hmxl8Sip3xImSkqpc4wETzuQ7CLzVOwPvwI6y/gVEm0pxRhtJSq8SzfOp26eJWl80u8v78Osg==} + engines: {node: '>=18'} + + '@sentry/svelte@10.75.0': + resolution: {integrity: sha512-VFsAdiNvIOY5vc+Bcx2SazSFT8d6EqT7vuGsEcxVLie5rRZQ4f88WRTdZsg0xOovCmuXhAtw5PJ1SwDEJBzvEw==} + engines: {node: '>=18'} + peerDependencies: + svelte: 3.x || 4.x || 5.x + + '@sentry/sveltekit@10.75.0': + resolution: {integrity: sha512-0y7xMfb4BoPav6jSRS4OCnp3UqCqeUKGhBp9N3e/tBlFdiLdnJtGkDIgkxIlCDAIFsutBzZQSOgrGhgHX4UqPA==} + engines: {node: '>=18'} + peerDependencies: + '@sveltejs/kit': 2.x || ^3.0.0-0 + vite: '*' + peerDependenciesMeta: + vite: + optional: true + + '@sentry/vite-plugin@5.4.0': + resolution: {integrity: sha512-fFJgCxs5hDyAm9BbZJ+LbA+LK2tjX5OoD0v0ARU4StR6KQmGUduoPs69yJ9AfqZ0om3Rlp5JDliiwFcNkasORA==} + engines: {node: '>= 18'} + '@shikijs/core@4.3.1': resolution: {integrity: sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA==} engines: {node: '>=20'} @@ -1789,6 +2006,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1842,6 +2063,10 @@ packages: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -2002,6 +2227,10 @@ packages: blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + brace-expansion@5.0.12: + resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} + engines: {node: 20 || >=22} + browserslist@4.28.6: resolution: {integrity: sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -2059,6 +2288,9 @@ packages: chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + cjs-module-lexer@2.2.1: + resolution: {integrity: sha512-Ca8swihM+/4yKecYHY52kgJd300hi2lADU/a1RxNTRe+RJ9jvqQlESpbz9DnG9mowez8qwXHB8qYdIUw9e+F5Q==} + cliui@6.0.0: resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} @@ -2100,6 +2332,9 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + core-js@3.50.0: + resolution: {integrity: sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -2178,6 +2413,9 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} + dompurify@3.4.15: + resolution: {integrity: sha512-EUBjM+B+lkDE41iE82DDSCfkoPGfXx8IxFxPMjNzm/Uk4xDet77rTN9wqlxlVg71kK7XGuUMv6wUxJUwwv+Xyw==} + domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -2402,6 +2640,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@3.0.2: + resolution: {integrity: sha512-BuIB67FngDSyQ/dpQNOZybwdEBDUGJQvOqwWr4ha/ufYiqzuEwPkKO2zLhRAgay28tStRIHUeWmszZAJo3GCOg==} + esbuild@0.25.12: resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} @@ -2449,6 +2690,9 @@ packages: picomatch: optional: true + fflate@0.4.9: + resolution: {integrity: sha512-zdxgIEddhfsyCaWpJ2SdXEP8ZMrKJ6+5jl4OupODcywU0IhRk6gdXuVGcPICyfx2H97hVK7xmJtRLPjkxAX8Vw==} + file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} @@ -2460,6 +2704,10 @@ packages: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -2494,6 +2742,16 @@ packages: github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + globalyzer@0.1.0: + resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} + + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -2517,9 +2775,17 @@ packages: htmlparser2@10.1.0: resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + import-in-the-middle@3.5.1: + resolution: {integrity: sha512-mPKuL8bPQzecui2KK6Gb+M8JvJoHnhS1FeYGa22QopBmlevF5F0FE6ued/B5EgHDeIoMTONIpDWWFKUPOG0DBQ==} + engines: {node: '>=18'} + import-meta-resolve@4.2.0: resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} @@ -2567,6 +2833,9 @@ packages: resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} engines: {node: '>=16'} + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true @@ -2699,9 +2968,17 @@ packages: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lru-cache@11.5.3: + resolution: {integrity: sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -2739,9 +3016,17 @@ packages: engines: {node: '>=22.0.0'} hasBin: true + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} @@ -2753,6 +3038,9 @@ packages: peerDependencies: svelte: ^5.27.0 + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -2783,6 +3071,15 @@ packages: node-fetch-native@1.6.7: resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + node-releases@2.0.51: resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} engines: {node: '>=18'} @@ -2811,10 +3108,18 @@ packages: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -2835,6 +3140,10 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} @@ -2934,6 +3243,34 @@ packages: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} + posthog-js@1.434.2: + resolution: {integrity: sha512-xyvjj88U2Cp1JfDzmifLvNbdiY/z+YbTT1DaRRKRZ+qzaF9IKqMLumpv8jTKGh6y3m4vc3xz0ku8h5Tw0XU9zQ==} + peerDependencies: + '@types/react': '>=16.8.0' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + + posthog-node@5.52.4: + resolution: {integrity: sha512-P3p4OouGQfw/ouv+Px4gcVWLNwYsFB4JPyksYmxv7QRX4niwp1/C9XjQSUyl0nt+KoQnK14zFDfONHWVthwKDA==} + engines: {node: ^20.20.0 || >=22.22.0} + peerDependencies: + rxjs: ^7.0.0 + peerDependenciesMeta: + rxjs: + optional: true + + preact@10.29.8: + resolution: {integrity: sha512-ej2aVZ+vZ8WO7tvlQWRM9N63A0KzF9q4mWJfDUHgYaIofWY9hu74QdnQrjoPMmZi2/nZ5gN0bJCQF49xQqx09Q==} + peerDependencies: + preact-render-to-string: '>=5' + peerDependenciesMeta: + preact-render-to-string: + optional: true + prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} @@ -3007,6 +3344,10 @@ packages: engines: {node: '>=14'} hasBin: true + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -3014,6 +3355,9 @@ packages: property-information@7.2.0: resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + pump@3.0.4: resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} @@ -3032,6 +3376,9 @@ packages: quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + query-selector-shadow-dom@1.0.1: + resolution: {integrity: sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==} + query-string@9.4.1: resolution: {integrity: sha512-lSyJeN3RuaG7DZGWThtYRhk96+kEyZ/+doZpERuWbjeFL+Ok3vEat/swU498rAI0NcVt5/RJp8UDuLz7FckxrA==} engines: {node: '>=18'} @@ -3078,6 +3425,10 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-in-the-middle@8.0.1: + resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} + engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} + require-main-filename@2.0.0: resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} @@ -3206,6 +3557,10 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + sorcery@1.0.0: + resolution: {integrity: sha512-5ay9oJE+7sNmhzl3YNG18jEEEf4AOQCM/FAqR5wMmzqd1FtRorFbJXn3w3SKOhbiQaVgHM+Q1lszZspjri7bpA==} + hasBin: true + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -3324,6 +3679,9 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} + tiny-glob@0.2.9: + resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} + tinyexec@1.2.4: resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} engines: {node: '>=18'} @@ -3336,6 +3694,9 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -3488,12 +3849,29 @@ packages: vite: optional: true + web-vitals@6.2.1: + resolution: {integrity: sha512-rLcLXA2sx6+9dE88NHFubwTtGxpK4yYBLj6qHPdFoCaLr0cXGb4efOqtKLlm4loGA4OEKHIQKMKzZkKyOh5ctw==} + + web-vitals@6.2.2: + resolution: {integrity: sha512-oto5x6dLEgrRqfcWed+pZEUb2q6ikbFmqF54CRDhI/QGbn+qxC49b4C4OkbH+kb9C3a8shpFD3SgR9kRcs80ZQ==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which-module@2.0.1: resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + workerd@1.20260710.1: resolution: {integrity: sha512-U2sBPPrb9U97sBKnnMN6Kv8p65903P35nwMkPE9vSH/bRuRqkZ3a1EjUw3jV28RhiyXpkLF77Evzw8XimFxyTw==} engines: {node: '>=16'} @@ -3559,6 +3937,10 @@ packages: resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} engines: {node: '>=8'} + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + yocto-spinner@0.2.3: resolution: {integrity: sha512-sqBChb33loEnkoXte1bLg45bEBsOP9N1kzQh5JZNKj/0rik4zAPTNSAVPj3uQAdc6slYJ0Ksc403G2XgxsJQFQ==} engines: {node: '>=18.19'} @@ -4515,6 +4897,47 @@ snapshots: '@noble/hashes@2.2.0': {} + '@opentelemetry/api-logs@0.220.0': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/api@1.9.1': {} + + '@opentelemetry/core@2.11.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.220.0 + import-in-the-middle: 3.5.1 + require-in-the-middle: 8.0.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/sdk-trace@2.11.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.43.0 + '@opentelemetry/semantic-conventions@1.43.0': {} '@oxc-project/types@0.139.0': {} @@ -4639,6 +5062,17 @@ snapshots: '@poppinss/exception@1.2.3': {} + '@posthog/browser-common@0.9.0': + dependencies: + '@posthog/core': 1.55.0 + '@posthog/types': 1.412.2 + + '@posthog/core@1.55.0': + dependencies: + '@posthog/types': 1.412.2 + + '@posthog/types@1.412.2': {} + '@prisma/client@5.22.0': {} '@rolldown/binding-android-arm64@1.1.5': @@ -4816,6 +5250,188 @@ snapshots: domhandler: 5.0.3 selderee: 0.12.0 + '@sentry/browser-utils@10.75.0': + dependencies: + '@sentry/conventions': 0.16.0 + '@sentry/core': 10.75.0 + + '@sentry/browser@10.75.0': + dependencies: + '@sentry/browser-utils': 10.75.0 + '@sentry/conventions': 0.16.0 + '@sentry/core': 10.75.0 + '@sentry/feedback': 10.75.0 + '@sentry/replay': 10.75.0 + '@sentry/replay-canvas': 10.75.0 + + '@sentry/bundler-plugins@10.75.0': + dependencies: + '@babel/core': 7.29.7 + '@sentry/cli': 2.58.6 + '@sentry/core': 10.75.0 + dotenv: 17.4.2 + find-up: 5.0.0 + glob: 13.0.6 + magic-string: 0.30.21 + transitivePeerDependencies: + - encoding + - supports-color + + '@sentry/cli-darwin@2.58.6': + optional: true + + '@sentry/cli-linux-arm64@2.58.6': + optional: true + + '@sentry/cli-linux-arm@2.58.6': + optional: true + + '@sentry/cli-linux-i686@2.58.6': + optional: true + + '@sentry/cli-linux-x64@2.58.6': + optional: true + + '@sentry/cli-win32-arm64@2.58.6': + optional: true + + '@sentry/cli-win32-i686@2.58.6': + optional: true + + '@sentry/cli-win32-x64@2.58.6': + optional: true + + '@sentry/cli@2.58.6': + dependencies: + https-proxy-agent: 5.0.1 + node-fetch: 2.7.0 + progress: 2.0.3 + proxy-from-env: 1.1.0 + which: 2.0.2 + optionalDependencies: + '@sentry/cli-darwin': 2.58.6 + '@sentry/cli-linux-arm': 2.58.6 + '@sentry/cli-linux-arm64': 2.58.6 + '@sentry/cli-linux-i686': 2.58.6 + '@sentry/cli-linux-x64': 2.58.6 + '@sentry/cli-win32-arm64': 2.58.6 + '@sentry/cli-win32-i686': 2.58.6 + '@sentry/cli-win32-x64': 2.58.6 + transitivePeerDependencies: + - encoding + - supports-color + + '@sentry/cloudflare@10.75.0(@cloudflare/workers-types@5.20260717.1)(wrangler@4.111.0(@cloudflare/workers-types@5.20260717.1))': + dependencies: + '@opentelemetry/api': 1.9.1 + '@sentry/core': 10.75.0 + '@sentry/server-utils': 10.75.0 + magic-string: 0.30.21 + optionalDependencies: + '@cloudflare/workers-types': 5.20260717.1 + wrangler: 4.111.0(@cloudflare/workers-types@5.20260717.1) + + '@sentry/conventions@0.16.0': {} + + '@sentry/core@10.75.0': + dependencies: + '@sentry/conventions': 0.16.0 + + '@sentry/feedback@10.75.0': + dependencies: + '@sentry/core': 10.75.0 + + '@sentry/node-core@10.75.0(@opentelemetry/api@1.9.1)(@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))': + dependencies: + '@sentry/conventions': 0.16.0 + '@sentry/core': 10.75.0 + '@sentry/opentelemetry': 10.75.0(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1)) + import-in-the-middle: 3.5.1 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.11.0(@opentelemetry/api@1.9.1) + + '@sentry/node@10.75.0': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.11.0(@opentelemetry/api@1.9.1) + '@sentry/conventions': 0.16.0 + '@sentry/core': 10.75.0 + '@sentry/node-core': 10.75.0(@opentelemetry/api@1.9.1)(@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1)) + '@sentry/opentelemetry': 10.75.0(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1)) + '@sentry/server-utils': 10.75.0 + import-in-the-middle: 3.5.1 + transitivePeerDependencies: + - '@opentelemetry/core' + - '@opentelemetry/exporter-trace-otlp-http' + - supports-color + + '@sentry/opentelemetry@10.75.0(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/sdk-trace-base': 2.11.0(@opentelemetry/api@1.9.1) + '@sentry/conventions': 0.16.0 + '@sentry/core': 10.75.0 + + '@sentry/replay-canvas@10.75.0': + dependencies: + '@sentry/core': 10.75.0 + '@sentry/replay': 10.75.0 + + '@sentry/replay@10.75.0': + dependencies: + '@sentry/browser-utils': 10.75.0 + '@sentry/core': 10.75.0 + + '@sentry/server-utils@10.75.0': + dependencies: + '@sentry/conventions': 0.16.0 + '@sentry/core': 10.75.0 + + '@sentry/svelte@10.75.0(svelte@5.56.6)': + dependencies: + '@sentry/browser': 10.75.0 + '@sentry/core': 10.75.0 + magic-string: 0.30.21 + svelte: 5.56.6 + + '@sentry/sveltekit@10.75.0(@cloudflare/workers-types@5.20260717.1)(@sveltejs/kit@2.69.3(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.6)(vite@8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.6)(typescript@6.0.3)(vite@8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.6)(vite@8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.9.0))(wrangler@4.111.0(@cloudflare/workers-types@5.20260717.1))': + dependencies: + '@sentry/cloudflare': 10.75.0(@cloudflare/workers-types@5.20260717.1)(wrangler@4.111.0(@cloudflare/workers-types@5.20260717.1)) + '@sentry/conventions': 0.16.0 + '@sentry/core': 10.75.0 + '@sentry/node': 10.75.0 + '@sentry/svelte': 10.75.0(svelte@5.56.6) + '@sentry/vite-plugin': 5.4.0 + '@sveltejs/acorn-typescript': 1.0.11(acorn@8.17.0) + '@sveltejs/kit': 2.69.3(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.6)(vite@8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.6)(typescript@6.0.3)(vite@8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.9.0)) + acorn: 8.17.0 + magic-string: 0.30.21 + sorcery: 1.0.0 + optionalDependencies: + vite: 8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.9.0) + transitivePeerDependencies: + - '@cloudflare/workers-types' + - '@opentelemetry/core' + - '@opentelemetry/exporter-trace-otlp-http' + - encoding + - rollup + - supports-color + - svelte + - webpack + - wrangler + + '@sentry/vite-plugin@5.4.0': + dependencies: + '@sentry/bundler-plugins': 10.75.0 + transitivePeerDependencies: + - encoding + - rollup + - supports-color + - webpack + '@shikijs/core@4.3.1': dependencies: '@shikijs/primitive': 4.3.1 @@ -5072,6 +5688,12 @@ snapshots: acorn@8.17.0: {} + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + ansi-regex@5.0.1: {} ansi-styles@4.3.0: @@ -5108,6 +5730,8 @@ snapshots: axobject-query@4.1.0: {} + balanced-match@4.0.4: {} + base64-js@1.5.1: {} baseline-browser-mapping@2.10.43: {} @@ -5205,6 +5829,10 @@ snapshots: blake3-wasm@2.1.5: {} + brace-expansion@5.0.12: + dependencies: + balanced-match: 4.0.4 + browserslist@4.28.6: dependencies: baseline-browser-mapping: 2.10.43 @@ -5270,6 +5898,8 @@ snapshots: chownr@1.1.4: {} + cjs-module-lexer@2.2.1: {} + cliui@6.0.0: dependencies: string-width: 4.2.3 @@ -5300,6 +5930,8 @@ snapshots: cookie@0.7.2: {} + core-js@3.50.0: {} + debug@4.4.3: dependencies: ms: 2.1.3 @@ -5355,6 +5987,10 @@ snapshots: dependencies: domelementtype: 2.3.0 + dompurify@3.4.15: + optionalDependencies: + '@types/trusted-types': 2.0.7 + domutils@3.2.2: dependencies: dom-serializer: 2.0.0 @@ -5411,6 +6047,8 @@ snapshots: es-errors@1.3.0: {} + es-module-lexer@3.0.2: {} + esbuild@0.25.12: optionalDependencies: '@esbuild/aix-ppc64': 0.25.12 @@ -5489,6 +6127,8 @@ snapshots: optionalDependencies: picomatch: 4.0.5 + fflate@0.4.9: {} + file-uri-to-path@1.0.0: {} filter-obj@5.1.0: {} @@ -5498,6 +6138,11 @@ snapshots: locate-path: 5.0.0 path-exists: 4.0.0 + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + fs-constants@1.0.0: {} fsevents@2.3.2: @@ -5520,6 +6165,16 @@ snapshots: github-from-package@0.0.0: {} + glob@13.0.6: + dependencies: + minimatch: 10.2.6 + minipass: 7.1.3 + path-scurry: 2.0.2 + + globalyzer@0.1.0: {} + + globrex@0.1.2: {} + graceful-fs@4.2.11: {} hasown@2.0.4: @@ -5561,8 +6216,21 @@ snapshots: domutils: 3.2.2 entities: 7.0.1 + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + ieee754@1.2.1: {} + import-in-the-middle@3.5.1: + dependencies: + cjs-module-lexer: 2.2.1 + es-module-lexer: 3.0.2 + module-details-from-path: 1.0.4 + import-meta-resolve@4.2.0: {} inherits@2.0.4: {} @@ -5599,6 +6267,8 @@ snapshots: dependencies: is-inside-container: 1.0.0 + isexe@2.0.0: {} + jiti@2.7.0: {} jose@6.2.3: {} @@ -5684,8 +6354,14 @@ snapshots: dependencies: p-locate: 4.1.0 + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + lodash@4.17.21: {} + lru-cache@11.5.3: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -5739,8 +6415,14 @@ snapshots: - bufferutil - utf-8-validate + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.12 + minimist@1.2.8: {} + minipass@7.1.3: {} + mkdirp-classic@0.5.3: {} mlly@1.8.2: @@ -5756,6 +6438,8 @@ snapshots: svelte: 5.56.6 svelte-toolbelt: 0.7.1(svelte@5.56.6) + module-details-from-path@1.0.4: {} + mri@1.2.0: {} mrmime@2.0.1: {} @@ -5774,6 +6458,10 @@ snapshots: node-fetch-native@1.6.7: {} + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + node-releases@2.0.51: {} obug@2.1.4: {} @@ -5803,10 +6491,18 @@ snapshots: dependencies: p-try: 2.2.0 + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + p-locate@4.1.0: dependencies: p-limit: 2.3.0 + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + p-try@2.2.0: {} package-manager-detector@1.7.0: {} @@ -5824,6 +6520,11 @@ snapshots: path-parse@1.0.7: {} + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.3 + minipass: 7.1.3 + path-to-regexp@6.3.0: {} pathe@2.0.3: {} @@ -5913,6 +6614,27 @@ snapshots: dependencies: xtend: 4.0.2 + posthog-js@1.434.2: + dependencies: + '@posthog/browser-common': 0.9.0 + '@posthog/core': 1.55.0 + '@posthog/types': 1.412.2 + core-js: 3.50.0 + dompurify: 3.4.15 + fflate: 0.4.9 + preact: 10.29.8 + query-selector-shadow-dom: 1.0.1 + web-vitals: 6.2.2 + web-vitals-soft-navs: web-vitals@6.2.1 + transitivePeerDependencies: + - preact-render-to-string + + posthog-node@5.52.4: + dependencies: + '@posthog/core': 1.55.0 + + preact@10.29.8: {} + prebuild-install@7.1.3: dependencies: detect-libc: 2.1.2 @@ -5941,6 +6663,8 @@ snapshots: prettier@3.9.5: {} + progress@2.0.3: {} + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -5948,6 +6672,8 @@ snapshots: property-information@7.2.0: {} + proxy-from-env@1.1.0: {} + pump@3.0.4: dependencies: end-of-stream: 1.4.5 @@ -5967,6 +6693,8 @@ snapshots: quansync@0.2.11: {} + query-selector-shadow-dom@1.0.1: {} + query-string@9.4.1: dependencies: decode-uri-component: 0.4.1 @@ -6013,6 +6741,13 @@ snapshots: require-directory@2.1.1: {} + require-in-the-middle@8.0.1: + dependencies: + debug: 4.4.3 + module-details-from-path: 1.0.4 + transitivePeerDependencies: + - supports-color + require-main-filename@2.0.0: {} resend@6.17.2: @@ -6205,6 +6940,12 @@ snapshots: sisteransi@1.0.5: {} + sorcery@1.0.0: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + minimist: 1.2.8 + tiny-glob: 0.2.9 + source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -6342,6 +7083,11 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + tiny-glob@0.2.9: + dependencies: + globalyzer: 0.1.0 + globrex: 0.1.2 + tinyexec@1.2.4: {} tinyglobby@0.2.17: @@ -6351,6 +7097,8 @@ snapshots: totalist@3.0.1: {} + tr46@0.0.3: {} + trim-lines@3.0.1: {} tslib@1.14.1: {} @@ -6466,10 +7214,25 @@ snapshots: optionalDependencies: vite: 8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.9.0) + web-vitals@6.2.1: {} + + web-vitals@6.2.2: {} + + webidl-conversions@3.0.1: {} + webpack-virtual-modules@0.6.2: {} + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + which-module@2.0.1: {} + which@2.0.2: + dependencies: + isexe: 2.0.0 + workerd@1.20260710.1: optionalDependencies: '@cloudflare/workerd-darwin-64': 1.20260710.1 @@ -6541,6 +7304,8 @@ snapshots: y18n: 4.0.3 yargs-parser: 18.1.3 + yocto-queue@0.1.0: {} + yocto-spinner@0.2.3: dependencies: yoctocolors: 2.1.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d136b91..f42b51a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,7 +7,9 @@ overrides: '@esbuild-kit/core-utils>esbuild': ^0.25.0 allowBuilds: '@prisma/client': false + '@sentry/cli': true better-sqlite3: true + core-js: false esbuild: true sharp: true workerd: true @@ -15,4 +17,3 @@ autoInstallPeers: false dedupePeerDependents: false minimumReleaseAge: 0 sideEffectsCache: true -