Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
45cdfed
docs(gtm): spec for analytics-foundation-1d (website reconciliation)
blove May 16, 2026
469f315
docs(gtm): revise Spec 1D to use Next.js rewrites for posthog-js proxy
blove May 16, 2026
707afe4
docs(gtm): implementation plan for analytics-foundation-1d (website r…
blove May 16, 2026
ec9f32c
feat(telemetry): add shared properties helpers (toSafeAnalyticsString…
blove May 16, 2026
748eed5
feat(telemetry): add browser shouldCaptureAnalytics + isLocalAnalytic…
blove May 16, 2026
b0b49af
refactor(website): import shared analytics helpers from @ngaf/telemet…
blove May 16, 2026
973b67e
refactor(website): import shouldCaptureAnalytics from @ngaf/telemetry…
blove May 16, 2026
6fce5ec
refactor(website): delete duplicated analytics/properties.ts
blove May 16, 2026
b2f48ea
refactor(cockpit): consume shouldCaptureAnalytics from @ngaf/telemetr…
blove May 16, 2026
7c88dad
feat(website): add /ingest/* rewrites to posthog proxy (#1D.1)
blove May 16, 2026
e28369c
feat(website): point posthog-js at /ingest proxy (#1D.2)
blove May 16, 2026
314f344
feat(cockpit): /ingest rewrites + CORS for iframe posthog-js
blove May 16, 2026
3acafd8
feat(cockpit): point posthog-js at /ingest proxy
blove May 16, 2026
8598e5a
feat(cockpit): pass /ingest proxy host to iframe runtime
blove May 16, 2026
f0540ca
docs(env): document NEXT_PUBLIC_COCKPIT_INGEST_HOST + NEXT_PUBLIC_COC…
blove May 16, 2026
6f522f0
test(telemetry): cover bracketed IPv6 [::1]:port for isLocalAnalytics…
blove May 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,14 @@ POSTHOG_PROJECT_ID=

# Cockpit shell analytics (apps/cockpit)
NEXT_PUBLIC_COCKPIT_POSTHOG_TOKEN=
NEXT_PUBLIC_COCKPIT_POSTHOG_HOST=https://us.i.posthog.com
NEXT_PUBLIC_COCKPIT_CAPTURE_LOCAL=false

# Cockpit iframe → cockpit-shell /ingest proxy (Spec 1D).
# Production: full absolute URL (e.g. https://cockpit.cacheplane.ai/ingest).
# Leave empty in dev to let RunMode derive it from window.location.origin.
NEXT_PUBLIC_COCKPIT_INGEST_HOST=

# CORS origin allowed to POST to cockpit's /ingest from iframes (Spec 1D).
# Production: https://examples.cacheplane.ai
# Leave empty in dev — wildcard '*' is used.
NEXT_PUBLIC_COCKPIT_IFRAME_ORIGIN=
5 changes: 3 additions & 2 deletions apps/cockpit/instrumentation-client.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
// SPDX-License-Identifier: MIT
import posthog from 'posthog-js';
import { getCockpitSessionId } from './src/lib/analytics/distinct-id';
import { shouldCaptureAnalytics } from './src/lib/analytics/properties';
import { shouldCaptureAnalytics } from '@ngaf/telemetry/browser';

const token = process.env.NEXT_PUBLIC_COCKPIT_POSTHOG_TOKEN;
const captureLocal = process.env.NEXT_PUBLIC_COCKPIT_CAPTURE_LOCAL === 'true';
const host = typeof window === 'undefined' ? undefined : window.location.host;

if (shouldCaptureAnalytics({ token, captureLocal, host })) {
posthog.init(token!, {
api_host: process.env.NEXT_PUBLIC_COCKPIT_POSTHOG_HOST ?? 'https://us.i.posthog.com',
api_host: '/ingest',
ui_host: 'https://us.posthog.com',
persistence: 'memory',
bootstrap: { distinctID: getCockpitSessionId() },
autocapture: false,
Expand Down
32 changes: 32 additions & 0 deletions apps/cockpit/next.config.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: MIT
import { describe, expect, it } from 'vitest';
import { nextConfig as config } from './next.config';

describe('cockpit next.config', () => {
it('exposes posthog-js rewrites under /ingest', async () => {
expect(typeof config.rewrites).toBe('function');
const rewrites = await config.rewrites!();
const list = Array.isArray(rewrites) ? rewrites : rewrites.beforeFiles ?? [];
const sources = list.map((r: { source: string }) => r.source);
expect(sources).toContain('/ingest/static/:path*');
expect(sources).toContain('/ingest/:path*');
const staticRule = list.find((r: { source: string }) => r.source === '/ingest/static/:path*');
expect(staticRule.destination).toBe('https://us-assets.i.posthog.com/static/:path*');
const apiRule = list.find((r: { source: string }) => r.source === '/ingest/:path*');
expect(apiRule.destination).toBe('https://us.i.posthog.com/:path*');
});

it('attaches CORS headers to /ingest/* responses', async () => {
expect(typeof config.headers).toBe('function');
const rules = await config.headers!();
const ingestRule = rules.find((r: { source: string }) => r.source === '/ingest/:path*');
expect(ingestRule).toBeDefined();
const headerKeys = ingestRule.headers.map((h: { key: string }) => h.key);
expect(headerKeys).toContain('Access-Control-Allow-Origin');
expect(headerKeys).toContain('Access-Control-Allow-Methods');
expect(headerKeys).toContain('Access-Control-Allow-Headers');
expect(headerKeys).toContain('Access-Control-Max-Age');
const methods = ingestRule.headers.find((h: { key: string }) => h.key === 'Access-Control-Allow-Methods');
expect(methods.value).toBe('POST, OPTIONS');
});
});
27 changes: 26 additions & 1 deletion apps/cockpit/next.config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,33 @@
import { composePlugins, withNx } from '@nx/next';
import type { WithNxOptions } from '@nx/next/plugins/with-nx';

const nextConfig: WithNxOptions = {
export const nextConfig: WithNxOptions = {
nx: {},
skipTrailingSlashRedirect: true,
rewrites: async () => [
{
source: '/ingest/static/:path*',
destination: 'https://us-assets.i.posthog.com/static/:path*',
},
{
source: '/ingest/:path*',
destination: 'https://us.i.posthog.com/:path*',
},
],
headers: async () => [
{
source: '/ingest/:path*',
headers: [
{
key: 'Access-Control-Allow-Origin',
value: process.env.NEXT_PUBLIC_COCKPIT_IFRAME_ORIGIN ?? '*',
},
{ key: 'Access-Control-Allow-Methods', value: 'POST, OPTIONS' },
{ key: 'Access-Control-Allow-Headers', value: 'Content-Type, Authorization' },
{ key: 'Access-Control-Max-Age', value: '86400' },
],
},
],
};

const plugins = [withNx];
Expand Down
5 changes: 3 additions & 2 deletions apps/cockpit/src/components/analytics-bootstrap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { useEffect } from 'react';
import posthog from 'posthog-js';
import { getCockpitSessionId } from '../lib/analytics/distinct-id';
import { shouldCaptureAnalytics } from '../lib/analytics/properties';
import { shouldCaptureAnalytics } from '@ngaf/telemetry/browser';

/**
* Client-side analytics bootstrap. Initializes posthog-js once per
Expand All @@ -25,7 +25,8 @@ export function AnalyticsBootstrap(): null {
return;
}
posthog.init(token as string, {
api_host: process.env.NEXT_PUBLIC_COCKPIT_POSTHOG_HOST ?? 'https://us.i.posthog.com',
api_host: '/ingest',
ui_host: 'https://us.posthog.com',
persistence: 'memory',
bootstrap: { distinctID: getCockpitSessionId() },
autocapture: false,
Expand Down
6 changes: 4 additions & 2 deletions apps/cockpit/src/components/run-mode/run-mode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ function buildIframeSrc(runtimeUrl: string, capabilitySlug: string): string {
url.searchParams.set('cockpit_cap', capabilitySlug);
const phk = process.env.NEXT_PUBLIC_COCKPIT_POSTHOG_TOKEN;
if (phk) url.searchParams.set('cockpit_phk', phk);
const host = process.env.NEXT_PUBLIC_COCKPIT_POSTHOG_HOST;
if (host) url.searchParams.set('cockpit_host', host);
const ingestHost =
process.env.NEXT_PUBLIC_COCKPIT_INGEST_HOST
?? (typeof window !== 'undefined' ? `${window.location.origin}/ingest` : undefined);
if (ingestHost) url.searchParams.set('cockpit_host', ingestHost);
return url.toString();
}

Expand Down
45 changes: 0 additions & 45 deletions apps/cockpit/src/lib/analytics/properties.spec.ts

This file was deleted.

22 changes: 0 additions & 22 deletions apps/cockpit/src/lib/analytics/properties.ts

This file was deleted.

4 changes: 3 additions & 1 deletion apps/cockpit/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
"@/*": ["./src/*"],
"@ngaf/design-tokens": ["../../libs/design-tokens/src/index.ts"],
"@ngaf/ui-react": ["../../libs/ui-react/src/index.ts"],
"@ngaf/cockpit-registry": ["../../libs/cockpit-registry/src/index.ts"]
"@ngaf/cockpit-registry": ["../../libs/cockpit-registry/src/index.ts"],
"@ngaf/telemetry/shared": ["../../libs/telemetry/src/shared/public-api.ts"],
"@ngaf/telemetry/browser": ["../../libs/telemetry/src/browser/public-api.ts"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
Expand Down
2 changes: 1 addition & 1 deletion apps/cockpit/vite.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export default defineConfig({
test: {
environment: 'jsdom',
globals: true,
include: ['src/**/*.spec.ts', 'src/**/*.spec.tsx'],
include: ['src/**/*.spec.ts', 'src/**/*.spec.tsx', '*.spec.ts'],
setupFiles: ['./test-setup.ts'],
},
});
8 changes: 3 additions & 5 deletions apps/website/instrumentation-client.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import posthog from 'posthog-js';
import {
normalizePostHogHost,
shouldCaptureAnalytics,
} from './src/lib/analytics/properties';
import { shouldCaptureAnalytics } from '@ngaf/telemetry/browser';

const token = process.env.NEXT_PUBLIC_POSTHOG_TOKEN;
const captureLocal = process.env.NEXT_PUBLIC_POSTHOG_CAPTURE_LOCAL === 'true';
const browserHost = typeof window === 'undefined' ? undefined : window.location.host;

if (shouldCaptureAnalytics({ token, captureLocal, host: browserHost })) {
posthog.init(token!, {
api_host: normalizePostHogHost(process.env.NEXT_PUBLIC_POSTHOG_HOST),
api_host: '/ingest',
ui_host: 'https://us.posthog.com',
defaults: '2026-01-30',
capture_pageview: true,
person_profiles: 'always',
Expand Down
18 changes: 18 additions & 0 deletions apps/website/next.config.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: MIT
import { describe, expect, it } from 'vitest';
import { nextConfig as config } from './next.config';

describe('website next.config rewrites', () => {
it('exposes posthog-js rewrites under /ingest', async () => {
expect(typeof config.rewrites).toBe('function');
const rewrites = await config.rewrites!();
const list = Array.isArray(rewrites) ? rewrites : rewrites.beforeFiles ?? [];
const sources = list.map((r: { source: string }) => r.source);
expect(sources).toContain('/ingest/static/:path*');
expect(sources).toContain('/ingest/:path*');
const staticRule = list.find((r: { source: string }) => r.source === '/ingest/static/:path*');
expect(staticRule.destination).toBe('https://us-assets.i.posthog.com/static/:path*');
const apiRule = list.find((r: { source: string }) => r.source === '/ingest/:path*');
expect(apiRule.destination).toBe('https://us.i.posthog.com/:path*');
});
});
13 changes: 12 additions & 1 deletion apps/website/next.config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import { composePlugins, withNx } from '@nx/next';
import type { WithNxOptions } from '@nx/next/plugins/with-nx';

const nextConfig: WithNxOptions = {
export const nextConfig: WithNxOptions = {
// Use this to set Nx-specific options
// See: https://nx.dev/recipes/next/next-config-setup
nx: {},
skipTrailingSlashRedirect: true,
rewrites: async () => [
{
source: '/ingest/static/:path*',
destination: 'https://us-assets.i.posthog.com/static/:path*',
},
{
source: '/ingest/:path*',
destination: 'https://us.i.posthog.com/:path*',
},
],
};

const plugins = [
Expand Down
2 changes: 1 addition & 1 deletion apps/website/src/app/api/ingest/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { PostHog } from 'posthog-node';
import { NextRequest, NextResponse } from 'next/server';
import { normalizePostHogHost, toSafeAnalyticsString } from '../../../lib/analytics/properties';
import { normalizePostHogHost, toSafeAnalyticsString } from '@ngaf/telemetry/shared';

const PUBLIC_INGEST_KEY = 'phc_public_cacheplane_telemetry';

Expand Down
2 changes: 1 addition & 1 deletion apps/website/src/app/api/leads/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { sendEmail, FROM, NOTIFY_TO, addToAudience } from '../../../../lib/resen
import { loopsUpsertContact, loopsSendEvent } from '../../../../lib/loops';
import { leadNotificationHtml } from '../../../../emails/lead-notification';
import { captureLeadConversion } from '../../../lib/analytics/server';
import { getSourcePage } from '../../../lib/analytics/properties';
import { getSourcePage } from '@ngaf/telemetry/shared';

const LEADS_FILE = path.join(process.cwd(), 'data', 'leads.ndjson');

Expand Down
2 changes: 1 addition & 1 deletion apps/website/src/app/api/newsletter/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { sendEmail, FROM, addToAudience } from '../../../../lib/resend';
import { loopsUpsertContact, loopsSendEvent } from '../../../../lib/loops';
import { newsletterWelcomeHtml } from '../../../../emails/newsletter-welcome';
import { captureNewsletterConversion } from '../../../lib/analytics/server';
import { getSourcePage } from '../../../lib/analytics/properties';
import { getSourcePage } from '@ngaf/telemetry/shared';

export async function POST(req: NextRequest) {
let body: { email?: string };
Expand Down
2 changes: 1 addition & 1 deletion apps/website/src/app/api/whitepaper-signup/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { angularDownloadHtml } from '../../../../emails/angular-download';
import { renderDownloadHtml } from '../../../../emails/render-download';
import { chatDownloadHtml } from '../../../../emails/chat-download';
import { captureWhitepaperConversion } from '../../../lib/analytics/server';
import { getSourcePage } from '../../../lib/analytics/properties';
import { getSourcePage } from '@ngaf/telemetry/shared';

const SIGNUPS_FILE = path.join(process.cwd(), 'data', 'whitepaper-signups.ndjson');

Expand Down
2 changes: 1 addition & 1 deletion apps/website/src/lib/analytics/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import posthog from 'posthog-js';
import { analyticsEvents, type AnalyticsEventName, type AnalyticsProperties } from './events';
import { getSourcePage, toSafeAnalyticsString } from './properties';
import { getSourcePage, toSafeAnalyticsString } from '@ngaf/telemetry/shared';

function currentSourcePage(): string {
if (typeof window === 'undefined') return '/';
Expand Down
2 changes: 1 addition & 1 deletion apps/website/src/lib/analytics/server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
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';
import { getEmailDomain, normalizePostHogHost, toSafeAnalyticsString } from '@ngaf/telemetry/shared';

function getServerPostHogClient(): PostHog | null {
const token = toSafeAnalyticsString(process.env.NEXT_PUBLIC_POSTHOG_TOKEN, 500);
Expand Down
4 changes: 3 additions & 1 deletion apps/website/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
"../../libs/cockpit-registry/src/index.ts"
],
"@ngaf/cockpit-shell": ["../../libs/cockpit-shell/src/index.ts"],
"@ngaf/design-tokens": ["../../libs/design-tokens/src/index.ts"]
"@ngaf/design-tokens": ["../../libs/design-tokens/src/index.ts"],
"@ngaf/telemetry/shared": ["../../libs/telemetry/src/shared/public-api.ts"],
"@ngaf/telemetry/browser": ["../../libs/telemetry/src/browser/public-api.ts"]
}
},
"include": [
Expand Down
Loading
Loading