* track core product analytics flows Track auth, search, export, audit, and credit-consumption events with canonical route IDs so PostHog funnels and usage dashboards stay low-noise and privacy-safe. * fix: keep auth actions usable after session loss * refactor: simplify analytics and auth helpers - Replace isRecord/getActiveOrganizationId type guards with simple cast - Refactor getAnalyticsRouteContext from if/return chain to route tables - Replace toVerificationIssueType switch with zod enum - Merge duplicate credits_consume events into single event per API call - Merge two PostHogBootstrap useEffects into one * refactor: add projectId to middleware context to reduce boilerplate The requireProjectContext middleware now includes projectId directly, eliminating repeated manual construction of BillingCustomerContext objects across all server function handlers. * remove unused BILLING_* env var fallbacks from cost profile script * remove before_send event enrichment to preserve native PostHog URL tracking The before_send hook was stripping $pathname, $current_url, $referrer and other URL properties, which breaks PostHog web analytics dashboards, paths analysis, session replay, and attribution. The route_id/route_group injection it provided is unnecessary since PostHog already captures $pathname natively. * remove route mapping layer, pass raw redirect paths to analytics events The route ID registry (STATIC_ROUTES, PROJECT_ROUTES, getAnalyticsRouteContext, getRedirectRouteId) duplicated what PostHog already captures via $pathname. Replace redirect_route_id with redirect_to containing the raw path, and remove ~80 lines of route mapping infrastructure. * clean up analytics events: drop redundant submit events and derived properties - Remove search_submit events for keywords, domain overview, and backlinks (the search_complete events capture the meaningful outcome data) - Remove target_type from backlinks events (derived 1:1 from search_scope) - Remove result_limit from keyword research (requested limit, not useful alongside actual result_count) - Remove export_format from data:export events (always "csv") * refactor: inline wrappers, colocate helpers, deduplicate getActiveOrganizationId - Inline toVerificationIssueType into verify-email.tsx (single-use wrapper) - Move mapDataforseoPathToCreditFeature into dataforseoClient.ts (only consumer) - Extract shared getActiveOrganizationId into lib/auth-session.ts (was duplicated in __root.tsx and middleware/ensure-user/hosted.ts) - Rename shared/analytics.ts → shared/internal-user.ts (only email helpers remain after removing route mapping, verification, and dataforseo helpers) * remove internal user tracking and email domain properties Drop is_internal_user super property, email_domain person property, and all supporting code (shared/internal-user.ts, getEmailDomain, isInternalUserEmail). Simplifies initPostHog and identifyAnalyticsUser signatures. * remove backlinks:search_complete effect-based tracking The reactive useEffect + useRef dedup pattern added ~30 lines of plumbing inside a data hook for a single analytics event. Not worth the complexity. * simplify: replace manual type guards with zod, deduplicate posthog and sign-out helpers - Replace hand-rolled typeof checks in getActiveOrganizationId and isAuthenticatedServerFunctionContext with zod safeParse - Extract withPostHogClient helper to deduplicate client posthog wrapper - Move apiKey guard into getServerPostHogClient factory - Extract signOutAndRedirect to avoid duplicated sign-out logic - Drop derivable has_results from analytics events - Remove unnecessary path normalization in mapDataforseoPathToCreditFeature * fix: strip email from pageview URLs, restore sign-out guard, harden server posthog, fix path mapper - Sanitize $current_url on pageviews to remove email query param (PII) - Restore onSuccess for sign-out redirect to avoid bounce-back on failure - Swallow shutdown() errors so PostHog outages can't fail billed work - Rewrite mapDataforseoPathToCreditFeature to match real API path structure (path[1] = module, path[3] = endpoint) instead of scanning all segments * simplify: remove redundant refs in verify-email, infer middleware context type - Remove unnecessary useRef guards in verify-email effects (deps already prevent re-firing) - Use z.ZodType<EnsuredUserContext> annotation to infer return type instead of casting - Add comment explaining one-shot PostHog client on Workers * fix: reset PostHog identity on sign-out before redirect * fix: require POSTHOG_HOST env var instead of defaulting to us.i.posthog.com * fix: annotate url as unknown to satisfy no-unsafe-assignment * format
70 lines
1.8 KiB
TypeScript
70 lines
1.8 KiB
TypeScript
import { env } from "cloudflare:workers";
|
|
import { PostHog } from "posthog-node";
|
|
import { isHostedServerAuthMode } from "@/server/lib/runtime-env";
|
|
|
|
/** Returns a one-shot PostHog client, or null if the key is missing. Caller must shut down after use.
|
|
* A new instance per call is fine — this runs on Cloudflare Workers where construction cost is negligible. */
|
|
function getServerPostHogClient(): PostHog | null {
|
|
const apiKey = env.POSTHOG_PUBLIC_KEY?.trim();
|
|
const host = env.POSTHOG_HOST?.trim();
|
|
if (!apiKey || !host) return null;
|
|
|
|
return new PostHog(apiKey, {
|
|
host,
|
|
flushAt: 1,
|
|
flushInterval: 0,
|
|
});
|
|
}
|
|
|
|
export async function captureServerError(
|
|
error: unknown,
|
|
properties: Record<string, string | null | undefined> = {},
|
|
) {
|
|
if (!(await isHostedServerAuthMode())) {
|
|
return;
|
|
}
|
|
|
|
const client = getServerPostHogClient();
|
|
if (!client) return;
|
|
|
|
try {
|
|
await client.captureExceptionImmediate(error, undefined, {
|
|
source: "server",
|
|
...properties,
|
|
});
|
|
} catch (posthogError) {
|
|
console.error("posthog server capture failed", posthogError);
|
|
} finally {
|
|
await client.shutdown().catch(() => {});
|
|
}
|
|
}
|
|
|
|
export async function captureServerEvent(args: {
|
|
distinctId: string;
|
|
event: string;
|
|
properties?: Record<string, unknown>;
|
|
organizationId: string;
|
|
}) {
|
|
if (!(await isHostedServerAuthMode())) {
|
|
return;
|
|
}
|
|
|
|
const client = getServerPostHogClient();
|
|
if (!client) return;
|
|
|
|
try {
|
|
client.capture({
|
|
distinctId: args.distinctId,
|
|
event: args.event,
|
|
properties: args.properties,
|
|
groups: {
|
|
organization: args.organizationId,
|
|
},
|
|
});
|
|
} catch (posthogError) {
|
|
console.error("posthog server capture failed", posthogError);
|
|
} finally {
|
|
await client.shutdown().catch(() => {});
|
|
}
|
|
}
|