trackFooterCta('Compliance', '/solutions/compliance')}
onMouseEnter={(e) => (e.currentTarget.style.color = tokens.colors.accent)}
onMouseLeave={(e) => (e.currentTarget.style.color = tokens.colors.textSecondary)}>
Compliance
trackFooterCta('Analytics', '/solutions/analytics')}
onMouseEnter={(e) => (e.currentTarget.style.color = tokens.colors.accent)}
onMouseLeave={(e) => (e.currentTarget.style.color = tokens.colors.textSecondary)}>
Analytics
trackFooterCta('Customer Support', '/solutions/customer-support')}
onMouseEnter={(e) => (e.currentTarget.style.color = tokens.colors.accent)}
onMouseLeave={(e) => (e.currentTarget.style.color = tokens.colors.textSecondary)}>
Customer Support
@@ -209,6 +262,7 @@ export function Footer() {
diff --git a/apps/website/src/components/shared/Nav.tsx b/apps/website/src/components/shared/Nav.tsx
index e82c91660..164819516 100644
--- a/apps/website/src/components/shared/Nav.tsx
+++ b/apps/website/src/components/shared/Nav.tsx
@@ -4,6 +4,7 @@ import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { tokens } from '@ngaf/design-tokens';
import { docsConfig } from '../../lib/docs-config';
+import { trackCtaClick, trackExternalLinkClick } from '../../lib/analytics/client';
const links = [
{ label: 'Pilot to Prod', href: '/pilot-to-prod', external: false },
@@ -89,6 +90,14 @@ export function Nav() {
});
const currentLib = docsConfig.find(lib => lib.id === mobileLibrary);
+ const trackNavLink = (label: string, href: string, external: boolean, surface: 'nav' | 'mobile_nav') => {
+ const ctaId = `${surface}_${label.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '')}`;
+ if (external) {
+ trackExternalLinkClick(href, { surface, cta_id: ctaId, cta_text: label });
+ return;
+ }
+ trackCtaClick({ surface, destination_url: href, cta_id: ctaId, cta_text: label });
+ };
return (
<>
@@ -112,6 +121,7 @@ export function Nav() {
trackNavLink(l.label, l.href, false, 'nav')}
className="text-sm font-mono transition-colors"
style={{ color: tokens.colors.textSecondary }}
onMouseEnter={(e) => (e.currentTarget.style.color = tokens.colors.accent)}
@@ -130,6 +141,11 @@ export function Nav() {
trackCtaClick({
+ surface: 'nav',
+ destination_url: '/pilot-to-prod#whitepaper-gate',
+ cta_id: 'nav_get_started',
+ cta_text: 'Get Started',
+ })}
style={{ background: tokens.colors.accent, color: '#fff' }}
onMouseEnter={(e) => (e.currentTarget.style.boxShadow = tokens.glow.button)}
onMouseLeave={(e) => (e.currentTarget.style.boxShadow = 'none')}>
@@ -222,7 +244,16 @@ export function Nav() {
setOpen(false)}
+ onClick={() => {
+ trackCtaClick({
+ surface: 'mobile_nav',
+ destination_url: `/docs/${currentLib.id}/${page.section}/${page.slug}`,
+ cta_id: 'mobile_nav_docs_page',
+ cta_text: page.title,
+ library: currentLib.id === 'agent' || currentLib.id === 'render' || currentLib.id === 'chat' ? currentLib.id : 'unknown',
+ });
+ setOpen(false);
+ }}
style={{
display: 'block', padding: '12px 14px', borderRadius: 8,
fontSize: 16, lineHeight: '24px', minHeight: 44,
@@ -251,7 +282,10 @@ export function Nav() {
const extraProps = l.external ? { target: '_blank', rel: 'noopener noreferrer' } : {};
return (
setOpen(false)}
+ onClick={() => {
+ trackNavLink(l.label, l.href, l.external, 'mobile_nav');
+ setOpen(false);
+ }}
style={{
display: 'block', padding: '14px 14px', borderRadius: 8,
fontSize: 16, lineHeight: '24px', minHeight: 48,
@@ -265,7 +299,14 @@ export function Nav() {
})}
setOpen(false)}
+ onClick={() => {
+ trackExternalLinkClick('https://github.com/cacheplane/angular-agent-framework', {
+ surface: 'mobile_nav',
+ cta_id: 'mobile_nav_github',
+ cta_text: 'GitHub',
+ });
+ setOpen(false);
+ }}
style={{
display: 'flex', alignItems: 'center', gap: 10,
padding: '14px 14px', borderRadius: 8, minHeight: 48,
@@ -276,7 +317,15 @@ export function Nav() {
setOpen(false)}
+ onClick={() => {
+ trackCtaClick({
+ surface: 'mobile_nav',
+ destination_url: '/pilot-to-prod#whitepaper-gate',
+ cta_id: 'mobile_nav_get_started',
+ cta_text: 'Get Started',
+ });
+ setOpen(false);
+ }}
style={{
display: 'block', textAlign: 'center',
padding: '14px 24px', borderRadius: 8,
diff --git a/apps/website/src/lib/analytics/client.ts b/apps/website/src/lib/analytics/client.ts
new file mode 100644
index 000000000..0ccac187d
--- /dev/null
+++ b/apps/website/src/lib/analytics/client.ts
@@ -0,0 +1,38 @@
+'use client';
+
+import posthog from 'posthog-js';
+import { analyticsEvents, type AnalyticsEventName, type AnalyticsProperties } from './events';
+import { getSourcePage, toSafeAnalyticsString } from './properties';
+
+function currentSourcePage(): string {
+ if (typeof window === 'undefined') return '/';
+ return getSourcePage(window.location.href);
+}
+
+export function track(event: AnalyticsEventName, properties: AnalyticsProperties = {}) {
+ if (typeof window === 'undefined') return;
+ if (!posthog.__loaded) return;
+
+ posthog.capture(event, {
+ source_page: currentSourcePage(),
+ ...properties,
+ });
+}
+
+export function trackCtaClick(properties: AnalyticsProperties) {
+ track(analyticsEvents.marketingCtaClick, properties);
+}
+
+export function trackExternalLinkClick(destinationUrl: string, properties: AnalyticsProperties) {
+ track(analyticsEvents.marketingExternalLinkClick, {
+ destination_url: toSafeAnalyticsString(destinationUrl, 1000),
+ ...properties,
+ });
+}
+
+export function trackWhitepaperDownloadClick(paper: AnalyticsProperties['paper'], properties: AnalyticsProperties) {
+ track(analyticsEvents.marketingWhitepaperDownloadClick, {
+ paper,
+ ...properties,
+ });
+}
diff --git a/apps/website/src/lib/analytics/events.ts b/apps/website/src/lib/analytics/events.ts
new file mode 100644
index 000000000..a6099642c
--- /dev/null
+++ b/apps/website/src/lib/analytics/events.ts
@@ -0,0 +1,55 @@
+export const analyticsEvents = {
+ marketingCtaClick: 'marketing:cta_click',
+ marketingExternalLinkClick: 'marketing:external_link_click',
+ marketingWhitepaperDownloadClick: 'marketing:whitepaper_download_click',
+ marketingWhitepaperSignupSubmit: 'marketing:whitepaper_signup_submit',
+ marketingWhitepaperSignupSuccess: 'marketing:whitepaper_signup_success',
+ marketingWhitepaperSignupFail: 'marketing:whitepaper_signup_fail',
+ marketingLeadFormSubmit: 'marketing:lead_form_submit',
+ marketingLeadFormSuccess: 'marketing:lead_form_success',
+ marketingLeadFormFail: 'marketing:lead_form_fail',
+ marketingNewsletterSignupSubmit: 'marketing:newsletter_signup_submit',
+ marketingNewsletterSignupSuccess: 'marketing:newsletter_signup_success',
+ marketingNewsletterSignupFail: 'marketing:newsletter_signup_fail',
+ docsSearchSubmit: 'docs:search_submit',
+ docsSearchResultClick: 'docs:search_result_click',
+ docsCopyPromptClick: 'docs:copy_prompt_click',
+ docsCopyCodeClick: 'docs:copy_code_click',
+ docsTabSelect: 'docs:tab_select',
+ docsSidebarSectionToggle: 'docs:sidebar_section_toggle',
+} as const;
+
+export type AnalyticsEventName = (typeof analyticsEvents)[keyof typeof analyticsEvents];
+
+export type AnalyticsSurface =
+ | 'nav'
+ | 'mobile_nav'
+ | 'footer'
+ | 'home'
+ | 'pricing'
+ | 'docs'
+ | 'library_landing'
+ | 'solution'
+ | 'toast';
+
+export type AnalyticsLibrary = 'agent' | 'render' | 'chat' | 'unknown';
+
+export type WhitepaperId = 'overview' | 'angular' | 'render' | 'chat';
+
+export type AnalyticsProperties = {
+ source_page?: string;
+ source_section?: string;
+ destination_url?: string;
+ cta_id?: string;
+ cta_text?: string;
+ surface?: AnalyticsSurface;
+ library?: AnalyticsLibrary;
+ paper?: WhitepaperId;
+ email_domain?: string;
+ company?: string;
+ is_success?: boolean;
+ result_count?: number;
+ query_length?: number;
+ error_reason?: string;
+ [key: string]: string | number | boolean | undefined;
+};
diff --git a/apps/website/src/lib/analytics/properties.spec.ts b/apps/website/src/lib/analytics/properties.spec.ts
new file mode 100644
index 000000000..f85e5d16e
--- /dev/null
+++ b/apps/website/src/lib/analytics/properties.spec.ts
@@ -0,0 +1,48 @@
+import { describe, expect, it } from 'vitest';
+import {
+ getEmailDomain,
+ getSourcePage,
+ isLocalAnalyticsHost,
+ normalizePostHogHost,
+ shouldCaptureAnalytics,
+ toSafeAnalyticsString,
+} from './properties';
+
+describe('analytics properties', () => {
+ it('extracts a normalized email domain without retaining the address', () => {
+ expect(getEmailDomain('Jane.Smith@Example.COM ')).toBe('example.com');
+ expect(getEmailDomain('not-an-email')).toBeNull();
+ expect(getEmailDomain('')).toBeNull();
+ });
+
+ it('truncates safe analytics strings and drops blank values', () => {
+ expect(toSafeAnalyticsString(' hello ')).toBe('hello');
+ expect(toSafeAnalyticsString('abcdef', 3)).toBe('abc');
+ expect(toSafeAnalyticsString(' ')).toBeUndefined();
+ expect(toSafeAnalyticsString(42)).toBeUndefined();
+ });
+
+ it('normalizes source URLs to path, query, and hash only', () => {
+ expect(getSourcePage('https://ngaf.example/docs?utm_source=x#intro')).toBe('/docs?utm_source=x#intro');
+ expect(getSourcePage('/pricing')).toBe('/pricing');
+ expect(getSourcePage('not a url')).toBe('/');
+ });
+
+ it('detects local hosts for opt-in development capture', () => {
+ expect(isLocalAnalyticsHost('localhost:3000')).toBe(true);
+ expect(isLocalAnalyticsHost('127.0.0.1:3000')).toBe(true);
+ expect(isLocalAnalyticsHost('ngaf.example')).toBe(false);
+ });
+
+ it('requires a token and skips local capture unless explicitly enabled', () => {
+ expect(shouldCaptureAnalytics({ token: '', captureLocal: false, host: 'ngaf.example' })).toBe(false);
+ expect(shouldCaptureAnalytics({ token: 'ph_test', captureLocal: false, host: 'localhost:3000' })).toBe(false);
+ expect(shouldCaptureAnalytics({ token: 'ph_test', captureLocal: true, host: 'localhost:3000' })).toBe(true);
+ expect(shouldCaptureAnalytics({ token: 'ph_test', captureLocal: false, host: 'ngaf.example' })).toBe(true);
+ });
+
+ it('uses the PostHog US ingest host as the default host', () => {
+ expect(normalizePostHogHost(undefined)).toBe('https://us.i.posthog.com');
+ expect(normalizePostHogHost('https://eu.i.posthog.com/')).toBe('https://eu.i.posthog.com');
+ });
+});
diff --git a/apps/website/src/lib/analytics/properties.ts b/apps/website/src/lib/analytics/properties.ts
new file mode 100644
index 000000000..ca9866671
--- /dev/null
+++ b/apps/website/src/lib/analytics/properties.ts
@@ -0,0 +1,59 @@
+const DEFAULT_POSTHOG_HOST = 'https://us.i.posthog.com';
+
+export type CaptureConfig = {
+ token?: string;
+ captureLocal?: boolean;
+ host?: string;
+};
+
+export function toSafeAnalyticsString(value: unknown, maxLength = 200): string | undefined {
+ if (typeof value !== 'string') return undefined;
+ const trimmed = value.trim();
+ if (!trimmed) return undefined;
+ return trimmed.slice(0, maxLength);
+}
+
+export function getEmailDomain(email: unknown): string | null {
+ const value = toSafeAnalyticsString(email, 320);
+ if (!value) return null;
+
+ const atIndex = value.lastIndexOf('@');
+ if (atIndex <= 0 || atIndex === value.length - 1) return null;
+
+ const domain = value.slice(atIndex + 1).toLowerCase();
+ return domain.includes('.') ? domain : null;
+}
+
+export function getSourcePage(value: unknown): string {
+ const source = toSafeAnalyticsString(value, 2000);
+ if (!source) return '/';
+
+ if (source.startsWith('/')) return source;
+
+ try {
+ const url = new URL(source);
+ return `${url.pathname}${url.search}${url.hash}` || '/';
+ } catch {
+ return '/';
+ }
+}
+
+export function isLocalAnalyticsHost(host: unknown): boolean {
+ const value = toSafeAnalyticsString(host, 300)?.toLowerCase();
+ if (!value) return false;
+
+ const hostname = value.split(':')[0];
+ return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
+}
+
+export function shouldCaptureAnalytics({ token, captureLocal = false, host }: CaptureConfig): boolean {
+ if (!toSafeAnalyticsString(token, 500)) return false;
+ if (isLocalAnalyticsHost(host) && !captureLocal) return false;
+ return true;
+}
+
+export function normalizePostHogHost(host: unknown): string {
+ const value = toSafeAnalyticsString(host, 500);
+ if (!value) return DEFAULT_POSTHOG_HOST;
+ return value.endsWith('/') ? value.slice(0, -1) : value;
+}
diff --git a/apps/website/src/lib/analytics/server.ts b/apps/website/src/lib/analytics/server.ts
new file mode 100644
index 000000000..833e8507d
--- /dev/null
+++ b/apps/website/src/lib/analytics/server.ts
@@ -0,0 +1,111 @@
+import { createHash } from 'crypto';
+import { PostHog } from 'posthog-node';
+import { analyticsEvents, type AnalyticsEventName, type AnalyticsProperties, type WhitepaperId } from './events';
+import { getEmailDomain, normalizePostHogHost, toSafeAnalyticsString } from './properties';
+
+function getServerPostHogClient(): PostHog | null {
+ const token = toSafeAnalyticsString(process.env.NEXT_PUBLIC_POSTHOG_TOKEN, 500);
+ if (!token) return null;
+
+ return new PostHog(token, {
+ host: normalizePostHogHost(process.env.NEXT_PUBLIC_POSTHOG_HOST),
+ flushAt: 1,
+ flushInterval: 0,
+ });
+}
+
+function getHashedEmailDistinctId(email: unknown): string | null {
+ const value = toSafeAnalyticsString(email, 320)?.toLowerCase();
+ if (!value || !getEmailDomain(value)) return null;
+ return `email_sha256:${createHash('sha256').update(value).digest('hex')}`;
+}
+
+export async function captureServerEvent({
+ distinctId,
+ event,
+ properties,
+}: {
+ distinctId: string;
+ event: AnalyticsEventName;
+ properties?: AnalyticsProperties;
+}) {
+ const posthog = getServerPostHogClient();
+ if (!posthog) return;
+
+ let didShutdown = false;
+ try {
+ posthog.capture({
+ distinctId,
+ event,
+ properties,
+ });
+ await posthog.shutdown();
+ didShutdown = true;
+ } catch (err) {
+ console.error('[posthog] capture failed:', err);
+ } finally {
+ if (!didShutdown) {
+ await posthog.shutdown().catch(() => undefined);
+ }
+ }
+}
+
+export async function captureLeadConversion({
+ email,
+ company,
+ sourcePage,
+}: {
+ email: string;
+ company?: string;
+ sourcePage?: string;
+}) {
+ const distinctId = getHashedEmailDistinctId(email);
+ if (!distinctId) return;
+
+ await captureServerEvent({
+ distinctId,
+ event: analyticsEvents.marketingLeadFormSuccess,
+ properties: {
+ email_domain: getEmailDomain(email) ?? undefined,
+ company: toSafeAnalyticsString(company, 200),
+ source_page: sourcePage,
+ },
+ });
+}
+
+export async function captureWhitepaperConversion({
+ email,
+ paper,
+ sourcePage,
+}: {
+ email: string;
+ paper: WhitepaperId;
+ sourcePage?: string;
+}) {
+ const distinctId = getHashedEmailDistinctId(email);
+ if (!distinctId) return;
+
+ await captureServerEvent({
+ distinctId,
+ event: analyticsEvents.marketingWhitepaperSignupSuccess,
+ properties: {
+ email_domain: getEmailDomain(email) ?? undefined,
+ paper,
+ source_page: sourcePage,
+ },
+ });
+}
+
+export async function captureNewsletterConversion({ email, sourcePage }: { email: string; sourcePage?: string }) {
+ const distinctId = getHashedEmailDistinctId(email);
+ if (!distinctId) return;
+
+ await captureServerEvent({
+ distinctId,
+ event: analyticsEvents.marketingNewsletterSignupSuccess,
+ properties: {
+ email_domain: getEmailDomain(email) ?? undefined,
+ source_page: sourcePage,
+ },
+ });
+}
diff --git a/docs/superpowers/plans/2026-05-02-posthog-gtm-analytics.md b/docs/superpowers/plans/2026-05-02-posthog-gtm-analytics.md
new file mode 100644
index 000000000..3ef11ae8d
--- /dev/null
+++ b/docs/superpowers/plans/2026-05-02-posthog-gtm-analytics.md
@@ -0,0 +1,416 @@
+# PostHog GTM Analytics Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Add reliable website pageview, CTA, content, and lead conversion tracking for the AI startup GTM funnel using PostHog.
+
+**Architecture:** Initialize PostHog once in the Next.js app with `instrumentation-client.ts`, expose a small typed tracking layer for client events, and capture business-critical conversions from API routes with `posthog-node`. Use automatic `$pageview` for route traffic, static custom event names for GTM actions, and PostHog dashboards/funnels for activation analysis.
+
+**Tech Stack:** Next.js App Router, React 19, Nx, `posthog-js`, `posthog-node`, PostHog Cloud or self-hosted PostHog, Playwright for smoke coverage.
+
+---
+
+## Research Summary
+
+- PostHog's current Next.js guide recommends `posthog-js` client setup in `instrumentation-client.ts`, `NEXT_PUBLIC_POSTHOG_TOKEN`, `NEXT_PUBLIC_POSTHOG_HOST`, and `defaults: '2026-01-30'`.
+- PostHog recommends a reverse proxy for better delivery because tracking blockers can intercept direct PostHog requests.
+- PostHog's server-side Next.js guidance uses `posthog-node`, `flushAt: 1`, `flushInterval: 0`, and `await posthog.shutdown()` in short-lived server functions.
+- PostHog recommends installing tracking on both marketing site and product app to follow a user from first visit through product use.
+- PostHog best practices recommend lowercase snake_case event/property names, static event names, careful `distinct_id` design, and backend tracking when conversion accuracy matters.
+- Current npm versions checked on 2026-05-02: `posthog-js@1.372.6`, `posthog-node@5.33.0`, `@posthog/react@1.9.0`.
+
+Sources:
+- https://posthog.com/docs/libraries/next-js
+- https://posthog.com/docs/libraries/js
+- https://posthog.com/docs/product-analytics/best-practices
+
+## Current Repo Findings
+
+- Website app: `apps/website`, Next.js `~16.1.6`, React `^19.0.0`, App Router.
+- Global layout: `apps/website/src/app/layout.tsx`.
+- No existing PostHog, GA, Segment, or Plausible integration found.
+- Conversion APIs already exist:
+ - `apps/website/src/app/api/leads/route.ts`
+ - `apps/website/src/app/api/whitepaper-signup/route.ts`
+ - `apps/website/src/app/api/newsletter/route.ts`
+- GTM surfaces found:
+ - Top nav, footer, and mobile menu links.
+ - Landing page CTAs to docs, pilot, whitepapers, GitHub, npm, and cockpit.
+ - Whitepaper gates/downloads for overview, angular, render, and chat papers.
+ - Pricing enterprise lead form.
+ - Docs search, copy prompt/code actions, tabs, and sidebar navigation.
+
+## Analytics Scope
+
+Track the website funnel from anonymous visitor to qualified lead:
+
+1. Acquisition: pageviews, UTM/referrer preservation, landing page/category.
+2. Engagement: docs search, docs copy, CTA clicks, external product interest clicks, whitepaper download intent.
+3. Conversion: whitepaper signup, enterprise lead submission, newsletter signup.
+4. Qualification: company, email domain, paper/product interest, source page, landing page, campaign properties.
+5. Product handoff: outbound clicks to cockpit, npm, GitHub, and docs as intent signals.
+
+Out of scope for the first implementation:
+
+- Session replay default enablement.
+- Heatmaps.
+- A/B experiments or feature flags.
+- Tracking inside cockpit/product app unless a matching PostHog token and domain plan are defined.
+- Consent banner work beyond opt-out support unless legal requirements demand it.
+
+## Event Taxonomy
+
+Use static event names and variable properties.
+
+Core PostHog event:
+
+- `$pageview`: automatic pageview tracking.
+
+Custom GTM events:
+
+- `marketing:cta_click`
+- `marketing:external_link_click`
+- `marketing:whitepaper_download_click`
+- `marketing:whitepaper_signup_submit`
+- `marketing:whitepaper_signup_success`
+- `marketing:whitepaper_signup_fail`
+- `marketing:lead_form_submit`
+- `marketing:lead_form_success`
+- `marketing:lead_form_fail`
+- `marketing:newsletter_signup_submit`
+- `marketing:newsletter_signup_success`
+- `marketing:newsletter_signup_fail`
+- `docs:search_submit`
+- `docs:search_result_click`
+- `docs:copy_prompt_click`
+- `docs:copy_code_click`
+- `docs:tab_select`
+- `docs:sidebar_section_toggle`
+
+Shared properties:
+
+- `source_page`: current pathname.
+- `source_section`: stable section/component id where known.
+- `destination_url`: clicked URL where applicable.
+- `cta_id`: stable CTA id, e.g. `nav_get_started`, `hero_docs`, `footer_npm`.
+- `cta_text`: visible label where stable.
+- `surface`: `nav`, `mobile_nav`, `footer`, `home`, `pricing`, `docs`, `library_landing`, `solution`, `toast`.
+- `library`: `agent`, `render`, `chat`, or `unknown`.
+- `paper`: `overview`, `angular`, `render`, or `chat`.
+- `email_domain`: extracted server-side for conversion events, never raw email in generic click events.
+- `company`: only server-side on lead conversion events if acceptable for GTM reporting.
+- `is_success`: boolean for submit results if a generic event wrapper is preferred.
+
+Person and group properties:
+
+- Use anonymous tracking for pageviews and clicks.
+- On whitepaper/newsletter/lead success, call `identify` or server-side `capture` with email only if the team accepts email as the stable marketing identity. Otherwise use a generated lead id and set `email_domain`.
+- Avoid sending free-form `message` to PostHog. It may contain sensitive customer data.
+- Consider PostHog group analytics later for `company`, but do not add it in phase 1 unless company identity rules are clear.
+
+## File Structure
+
+- Create `apps/website/instrumentation-client.ts`
+ - Initializes browser PostHog once, skips local/dev capture unless explicitly enabled, and sets core config.
+
+- Create `apps/website/src/lib/analytics/events.ts`
+ - Defines event name constants, property types, and allowed CTA/surface ids.
+
+- Create `apps/website/src/lib/analytics/client.ts`
+ - Exports `track`, `trackCtaClick`, `trackExternalLinkClick`, `trackWhitepaperDownloadClick`, and helpers that no-op safely when PostHog is unavailable.
+
+- Create `apps/website/src/lib/analytics/server.ts`
+ - Exports `captureServerEvent`, `captureLeadConversion`, `captureWhitepaperConversion`, and `captureNewsletterConversion`.
+ - Wraps `posthog-node` and always calls `shutdown()`.
+
+- Create `apps/website/src/lib/analytics/properties.ts`
+ - Shared sanitizers for email domain, URL/path, paper id, UTM/campaign fields, and safe string truncation.
+
+- Modify `apps/website/.env.example`
+ - Add `NEXT_PUBLIC_POSTHOG_TOKEN`, `NEXT_PUBLIC_POSTHOG_HOST`, and `NEXT_PUBLIC_POSTHOG_CAPTURE_LOCAL=false`.
+
+- Modify `apps/website/package.json`
+ - Add `posthog-js` and `posthog-node`.
+
+- Modify high-value client components first:
+ - `apps/website/src/components/shared/Nav.tsx`
+ - `apps/website/src/components/shared/Footer.tsx`
+ - `apps/website/src/components/shared/AnnouncementToast.tsx`
+ - `apps/website/src/components/pricing/LeadForm.tsx`
+ - `apps/website/src/components/landing/WhitePaperSection.tsx`
+ - `apps/website/src/components/landing/WhitePaperGate.tsx`
+ - `apps/website/src/components/landing/angular/AngularWhitePaperGate.tsx`
+ - `apps/website/src/components/landing/render/RenderWhitePaperGate.tsx`
+ - `apps/website/src/components/landing/chat-landing/ChatLandingWhitePaperGate.tsx`
+ - `apps/website/src/components/docs/DocsSearch.tsx`
+ - `apps/website/src/components/docs/CopyPromptButton.tsx`
+ - `apps/website/src/components/docs/mdx/CodeBlock.tsx`
+
+- Modify conversion API routes:
+ - `apps/website/src/app/api/leads/route.ts`
+ - `apps/website/src/app/api/whitepaper-signup/route.ts`
+ - `apps/website/src/app/api/newsletter/route.ts`
+
+- Test files:
+ - Add unit tests for analytics property helpers if website test setup is available.
+ - Add Playwright request interception assertions in `apps/website/e2e/website.spec.ts` for no crashes and event attempts when env is configured.
+
+## Implementation Tasks
+
+### Task 1: Add PostHog Dependencies and Environment Contract
+
+**Files:**
+- Modify: `apps/website/package.json`
+- Modify: `apps/website/.env.example`
+- Modify: root lockfile
+
+- [ ] Add `posthog-js` and `posthog-node` to the website package dependencies.
+- [ ] Run `npm install` from the repo root.
+- [ ] Add env examples:
+ - `NEXT_PUBLIC_POSTHOG_TOKEN=`
+ - `NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com`
+ - `NEXT_PUBLIC_POSTHOG_CAPTURE_LOCAL=false`
+- [ ] Decide production host:
+ - Direct: `https://us.i.posthog.com` or EU equivalent.
+ - Preferred follow-up: reverse proxy path, e.g. `/ingest`, once hosting rules are known.
+- [ ] Verify lockfile diff only contains dependency changes.
+
+Run:
+
+```bash
+npm install
+npx nx build website
+```
+
+Expected:
+
+- Build succeeds.
+- No analytics events are sent without token configuration.
+
+### Task 2: Add Browser Initialization
+
+**Files:**
+- Create: `apps/website/instrumentation-client.ts`
+
+- [ ] Create `instrumentation-client.ts`.
+- [ ] Import `posthog-js`.
+- [ ] Guard initialization behind `NEXT_PUBLIC_POSTHOG_TOKEN`.
+- [ ] Skip localhost/127.0.0.1 unless `NEXT_PUBLIC_POSTHOG_CAPTURE_LOCAL=true`.
+- [ ] Configure `api_host`, `defaults: '2026-01-30'`, and conservative capture settings.
+- [ ] Leave pageview autocapture enabled for route/page tracking.
+- [ ] Do not enable session replay by default in this phase.
+
+Verification:
+
+```bash
+npx nx build website
+```
+
+Expected:
+
+- Build succeeds.
+- Generated client bundle contains no server-only env references.
+
+### Task 3: Add Typed Client Tracking Layer
+
+**Files:**
+- Create: `apps/website/src/lib/analytics/events.ts`
+- Create: `apps/website/src/lib/analytics/client.ts`
+- Create: `apps/website/src/lib/analytics/properties.ts`
+
+- [ ] Define event constants from the taxonomy above.
+- [ ] Define TypeScript property types for shared GTM properties.
+- [ ] Implement `track(event, properties)` that imports `posthog-js` and no-ops if unavailable.
+- [ ] Implement helper wrappers for common click events.
+- [ ] Implement safe helpers for path, destination URL, CTA ids, and paper ids.
+- [ ] Avoid dynamic event names.
+
+Verification:
+
+```bash
+npx nx build website
+```
+
+Expected:
+
+- TypeScript accepts all event/property definitions.
+
+### Task 4: Instrument Global Navigation and Footer
+
+**Files:**
+- Modify: `apps/website/src/components/shared/Nav.tsx`
+- Modify: `apps/website/src/components/shared/Footer.tsx`
+
+- [ ] Track desktop nav clicks with stable `cta_id`.
+- [ ] Track mobile menu open/close only if needed; otherwise track final link clicks, not every drawer interaction.
+- [ ] Track external links to GitHub, npm, and cockpit using `marketing:external_link_click`.
+- [ ] Track `Get Started` with `marketing:cta_click`.
+- [ ] Include `surface`, `source_page`, `destination_url`, and `cta_id`.
+
+Verification:
+
+```bash
+npx nx build website
+npx nx e2e website -- --grep "navigation"
+```
+
+Expected:
+
+- Existing navigation still works.
+- No hydration errors.
+
+### Task 5: Instrument Lead and Whitepaper Client Flows
+
+**Files:**
+- Modify: `apps/website/src/components/pricing/LeadForm.tsx`
+- Modify: `apps/website/src/components/shared/AnnouncementToast.tsx`
+- Modify: `apps/website/src/components/landing/WhitePaperSection.tsx`
+- Modify: `apps/website/src/components/landing/WhitePaperGate.tsx`
+- Modify: `apps/website/src/components/landing/angular/AngularWhitePaperGate.tsx`
+- Modify: `apps/website/src/components/landing/render/RenderWhitePaperGate.tsx`
+- Modify: `apps/website/src/components/landing/chat-landing/ChatLandingWhitePaperGate.tsx`
+
+- [ ] Track submit attempt before `fetch`.
+- [ ] Track client-side success/fail after API result.
+- [ ] Track direct PDF download clicks.
+- [ ] Include `paper`, `surface`, `source_page`, and `cta_id`.
+- [ ] Do not send raw email, name, company, or free-form message from client events.
+
+Verification:
+
+```bash
+npx nx build website
+```
+
+Expected:
+
+- Forms still submit.
+- Failed requests still show existing UI errors.
+
+### Task 6: Capture Server-Side Conversion Events
+
+**Files:**
+- Create: `apps/website/src/lib/analytics/server.ts`
+- Modify: `apps/website/src/app/api/leads/route.ts`
+- Modify: `apps/website/src/app/api/whitepaper-signup/route.ts`
+- Modify: `apps/website/src/app/api/newsletter/route.ts`
+
+- [ ] Add server PostHog client wrapper using `posthog-node`.
+- [ ] Return early when token/host is missing.
+- [ ] Capture conversion success after validation and persistence.
+- [ ] Capture failure events only for validation or pipeline errors that help GTM analysis.
+- [ ] Include `email_domain`, `company` for leads if approved, `paper`, and `source` fields.
+- [ ] Never send lead `message` content.
+- [ ] Always call `await posthog.shutdown()` after capture.
+
+Verification:
+
+```bash
+npx nx build website
+```
+
+Expected:
+
+- API routes compile.
+- Routes continue to return existing response shapes.
+
+### Task 7: Instrument Docs Intent Events
+
+**Files:**
+- Modify: `apps/website/src/components/docs/DocsSearch.tsx`
+- Modify: `apps/website/src/components/docs/CopyPromptButton.tsx`
+- Modify: `apps/website/src/components/docs/mdx/CodeBlock.tsx`
+- Modify: `apps/website/src/components/docs/mdx/Tabs.tsx`
+- Modify: `apps/website/src/components/docs/DocsSidebar.tsx`
+
+- [ ] Track docs searches with query length, result count, and active library, not raw query by default.
+- [ ] Track result clicks with destination path and library/section.
+- [ ] Track prompt/code copy events with doc path, language, and block id if available.
+- [ ] Track tab selection and sidebar toggles only when useful for content analysis.
+
+Verification:
+
+```bash
+npx nx build website
+```
+
+Expected:
+
+- No raw query/content is sent unless explicitly approved.
+- Build succeeds with docs instrumentation in client components.
+
+### Task 8: Add E2E Smoke Coverage
+
+**Files:**
+- Modify: `apps/website/e2e/website.spec.ts`
+
+- [ ] Add a smoke test with PostHog env vars enabled against a harmless test token placeholder or mocked route.
+- [ ] Intercept PostHog ingest host or reverse proxy path.
+- [ ] Visit `/`, `/pricing`, and one docs page.
+- [ ] Click one nav CTA and one docs copy/search action.
+- [ ] Assert the app stays usable and attempted analytics payloads include expected event names.
+- [ ] Avoid depending on real PostHog network in CI.
+
+Run:
+
+```bash
+npx nx e2e website
+```
+
+Expected:
+
+- E2E passes with network interception.
+
+### Task 9: Configure PostHog Project for GTM Use
+
+**Files:**
+- No code changes unless dashboard definitions are stored later.
+
+- [ ] Create PostHog project for website/product analytics.
+- [ ] Add production token and host to deployment environment.
+- [ ] Configure allowed domains.
+- [ ] Configure data retention appropriate to GTM reporting.
+- [ ] Decide whether person profiles are enabled for anonymous visitors or only identified leads.
+- [ ] Add dashboards:
+ - Website acquisition: pageviews, unique visitors, source/medium, landing page.
+ - Funnel: `$pageview` -> CTA click -> whitepaper signup -> lead form success.
+ - Content intent: docs pageviews, docs searches, copy events, library landing engagement.
+ - Outbound product intent: cockpit, GitHub, npm clicks.
+- [ ] Add saved funnels:
+ - Homepage to lead.
+ - Library landing to whitepaper signup.
+ - Docs to external install/repo click.
+ - Pricing to lead submission.
+
+### Task 10: Release and Monitoring
+
+**Files:**
+- Modify docs only if deployment runbook exists.
+
+- [ ] Deploy with token/host configured.
+- [ ] Open PostHog Live Events.
+- [ ] Visit production pages and trigger one safe test conversion if acceptable.
+- [ ] Confirm `$pageview`, CTA, download, and server conversion events appear.
+- [ ] Check event properties for accidental PII.
+- [ ] Create a short analytics taxonomy note if the team wants future contributors to follow it.
+- [ ] If docs/package guidance changes as part of the work, decide whether `npm run generate-agent-context` is needed. For analytics-only code, it should not be needed.
+
+## Privacy and Compliance Notes
+
+- Do not track raw form `message`, raw docs search query, or copied code content by default.
+- Treat email, name, company, and free-form customer text as sensitive.
+- If operating under stricter consent requirements, initialize with `opt_out_capturing_by_default: true` and wire a consent UI before sending events.
+- If the site targets EU users materially, choose EU PostHog host or document the data transfer decision.
+- Use backend conversion events for GTM truth; use frontend events for journey and interaction trends.
+
+## Recommended First Milestone
+
+Ship the smallest useful slice:
+
+1. PostHog client initialization.
+2. Automatic pageviews.
+3. Nav/footer/primary CTA clicks.
+4. Server-side lead, whitepaper, and newsletter conversion captures.
+5. One funnel dashboard in PostHog.
+
+This gives enough signal for GTM decisions without instrumenting every docs interaction on day one.
diff --git a/package-lock.json b/package-lock.json
index 553dec28a..fe4d52216 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -131,6 +131,8 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"next": "~16.1.6",
+ "posthog-js": "^1.372.6",
+ "posthog-node": "^5.20.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"remark-gfm": "^4.0.1",
@@ -139,6 +141,27 @@
"tailwind-merge": "^2.5.0"
}
},
+ "apps/website/node_modules/@posthog/core": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.9.1.tgz",
+ "integrity": "sha512-kRb1ch2dhQjsAapZmu6V66551IF2LnCbc1rnrQqnR7ArooVyJN9KOPXre16AJ3ObJz2eTfuP7x25BMyS2Y5Exw==",
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.6"
+ }
+ },
+ "apps/website/node_modules/posthog-node": {
+ "version": "5.20.0",
+ "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.20.0.tgz",
+ "integrity": "sha512-LkR5KfrvEQTnUtNKN97VxFB00KcYG1Iz8iKg8r0e/i7f1eQhg1WSZO+Jp1B4bvtHCmdpIE4HwYbvCCzFoCyjVg==",
+ "license": "MIT",
+ "dependencies": {
+ "@posthog/core": "1.9.1"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/@ag-ui/client": {
"version": "0.0.52",
"resolved": "https://registry.npmjs.org/@ag-ui/client/-/client-0.0.52.tgz",
@@ -15833,6 +15856,252 @@
}
}
},
+ "node_modules/@opentelemetry/api": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz",
+ "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/@opentelemetry/api-logs": {
+ "version": "0.208.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.208.0.tgz",
+ "integrity": "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/@opentelemetry/core": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz",
+ "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/exporter-logs-otlp-http": {
+ "version": "0.208.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.208.0.tgz",
+ "integrity": "sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api-logs": "0.208.0",
+ "@opentelemetry/core": "2.2.0",
+ "@opentelemetry/otlp-exporter-base": "0.208.0",
+ "@opentelemetry/otlp-transformer": "0.208.0",
+ "@opentelemetry/sdk-logs": "0.208.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/otlp-exporter-base": {
+ "version": "0.208.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.208.0.tgz",
+ "integrity": "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.2.0",
+ "@opentelemetry/otlp-transformer": "0.208.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/otlp-transformer": {
+ "version": "0.208.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.208.0.tgz",
+ "integrity": "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api-logs": "0.208.0",
+ "@opentelemetry/core": "2.2.0",
+ "@opentelemetry/resources": "2.2.0",
+ "@opentelemetry/sdk-logs": "0.208.0",
+ "@opentelemetry/sdk-metrics": "2.2.0",
+ "@opentelemetry/sdk-trace-base": "2.2.0",
+ "protobufjs": "^7.3.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
+ "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.2.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/resources": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz",
+ "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.7.1",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/core": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.1.tgz",
+ "integrity": "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-logs": {
+ "version": "0.208.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.208.0.tgz",
+ "integrity": "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api-logs": "0.208.0",
+ "@opentelemetry/core": "2.2.0",
+ "@opentelemetry/resources": "2.2.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.4.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
+ "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.2.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-metrics": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz",
+ "integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.2.0",
+ "@opentelemetry/resources": "2.2.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.9.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
+ "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.2.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace-base": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz",
+ "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.2.0",
+ "@opentelemetry/resources": "2.2.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/resources": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
+ "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.2.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/semantic-conventions": {
+ "version": "1.40.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz",
+ "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/@oxc-project/types": {
"version": "0.106.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.106.0.tgz",
@@ -16685,6 +16954,21 @@
"node": ">=18"
}
},
+ "node_modules/@posthog/core": {
+ "version": "1.28.0",
+ "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.28.0.tgz",
+ "integrity": "sha512-753giUMWuk602UtS101tDZuNcwiKkr+3UEhLgfOwHAk2W32n53knOxAjyWT0JwMq5/+0uSQ2y4uaZXQAxwvBSw==",
+ "license": "MIT",
+ "dependencies": {
+ "@posthog/types": "1.372.6"
+ }
+ },
+ "node_modules/@posthog/types": {
+ "version": "1.372.6",
+ "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.372.6.tgz",
+ "integrity": "sha512-sqI36LBvuo8xcYsXIlVa0q3IXJJjqtatM2LrXlyOM7kgHrldBwS4ldzaTXrTdpe/TiIl1b4ZHxtSHMzPig+DnQ==",
+ "license": "MIT"
+ },
"node_modules/@protobuf-ts/protoc": {
"version": "2.11.1",
"resolved": "https://registry.npmjs.org/@protobuf-ts/protoc/-/protoc-2.11.1.tgz",
@@ -16698,35 +16982,30 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
- "dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
- "dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/codegen": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
- "dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/eventemitter": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
- "dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/fetch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
- "dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.1",
@@ -16737,35 +17016,30 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
- "dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/inquire": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
- "dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
- "dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/pool": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
- "dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/utf8": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
- "dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@puppeteer/browsers": {
@@ -20069,7 +20343,6 @@
"version": "25.6.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.19.0"
@@ -20247,6 +20520,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
@@ -25053,6 +25333,17 @@
"node": ">=10.13.0"
}
},
+ "node_modules/core-js": {
+ "version": "3.49.0",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz",
+ "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
"node_modules/core-js-compat": {
"version": "3.49.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz",
@@ -26080,6 +26371,15 @@
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
+ "node_modules/dompurify": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.2.tgz",
+ "integrity": "sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
"node_modules/domutils": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
@@ -32565,7 +32865,6 @@
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
- "dev": true,
"license": "Apache-2.0"
},
"node_modules/long-timeout": {
@@ -37154,6 +37453,33 @@
"url": "https://github.com/sponsors/porsager"
}
},
+ "node_modules/posthog-js": {
+ "version": "1.372.6",
+ "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.372.6.tgz",
+ "integrity": "sha512-+Fy9fwWni5WDKQXiUBIzFvdmnZSR6OBxGC/4wj09JvvK5JE4dhI9ZlKO1+b887PowjeAx0sx1Tf+S1eAjDvzqg==",
+ "license": "SEE LICENSE IN LICENSE",
+ "dependencies": {
+ "@opentelemetry/api": "^1.9.0",
+ "@opentelemetry/api-logs": "^0.208.0",
+ "@opentelemetry/exporter-logs-otlp-http": "^0.208.0",
+ "@opentelemetry/resources": "^2.2.0",
+ "@opentelemetry/sdk-logs": "^0.208.0",
+ "@posthog/core": "1.28.0",
+ "@posthog/types": "1.372.6",
+ "core-js": "^3.38.1",
+ "dompurify": "^3.3.2",
+ "fflate": "^0.4.8",
+ "preact": "^10.28.2",
+ "query-selector-shadow-dom": "^1.0.1",
+ "web-vitals": "^5.1.0"
+ }
+ },
+ "node_modules/posthog-js/node_modules/fflate": {
+ "version": "0.4.8",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
+ "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==",
+ "license": "MIT"
+ },
"node_modules/powershell-utils": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz",
@@ -37167,6 +37493,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/preact": {
+ "version": "10.29.1",
+ "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz",
+ "integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/preact"
+ }
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -37375,7 +37711,6 @@
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz",
"integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==",
- "dev": true,
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -37619,6 +37954,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/query-selector-shadow-dom": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz",
+ "integrity": "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==",
+ "license": "MIT"
+ },
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -41669,7 +42010,6 @@
"version": "7.19.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
- "dev": true,
"license": "MIT"
},
"node_modules/unicode-canonical-property-names-ecmascript": {
@@ -42528,6 +42868,12 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/web-vitals": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.2.0.tgz",
+ "integrity": "sha512-i2z98bEmaCqSDiHEDu+gHl/dmR4Q+TxFmG3/13KkMO+o8UxQzCqWaDRCiLgEa41nlO4VpXSI0ASa1xWmO9sBlA==",
+ "license": "Apache-2.0"
+ },
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",