fix(analytics): restore migrated pricing funnel events (#7299)

* docs: design pricing plan exposure restoration

* fix(analytics): restore migrated pricing funnel events

* docs: design authenticated pricing analytics bridge

* docs: plan authenticated pricing analytics bridge

* feat(analytics): add authenticated pricing bridge contract

* fix(analytics): harden pricing bridge validation

* fix(analytics): restore complete personal pricing semantics

* fix(analytics): require explicit personal audience

* fix(analytics): relay authenticated pricing funnel events

* test(analytics): cover localized pricing handoff

* docs(analytics): correct pricing source handoff

* fix(analytics): align migrated pricing compatibility contract

---------

Co-authored-by: 张辉华 <zhanghuihua@refly.ai>
This commit is contained in:
open-design-crew[bot]
2026-08-24 08:08:05 +00:00
committed by GitHub
parent 1877bf607b
commit d517a18ec7
12 changed files with 2750 additions and 2 deletions
@@ -467,6 +467,14 @@ const countryOptions = buildCountryOptions(locale);
form.addEventListener('submit', async (event) => {
event.preventDefault();
// Pricing's legacy compatibility funnel counted submit intent, including
// attempts rejected by client validation. Keep this synchronous and
// payload-free so analytics can never delay or leak lead fields.
if (leadSource === 'pricing_team') {
form.dispatchEvent(new CustomEvent('pricing:enterprise-submit', {
bubbles: true,
}));
}
if (errorEl) errorEl.hidden = true;
let firstInvalid = null;
@@ -0,0 +1,434 @@
import {
GO_PLAN,
HOSTED_CLOUD_CONSOLE_DOMAINS,
PRICING_SNAPSHOT,
type BillingInterval,
type PlanTier,
type PlanTierConfig,
} from './pricing';
export type PricingBridgeSource = 'wallet' | 'dashboard';
export type PlanExposureInput = {
planId: PlanTier;
billingInterval: BillingInterval;
priceUsd: string;
creditsGrantedUsd: string;
deployLimit: number;
introOfferApplied: boolean;
firstMonthEligible: boolean;
isCurrentPlan: boolean;
isRecommended: boolean;
};
type ChangeIntervalClickInput = {
element: 'change_interval';
currentPlanId: PlanTier | null;
currentBillingInterval: BillingInterval;
targetBillingInterval: BillingInterval;
};
type PlanCtaClickFields = {
currentBillingInterval: BillingInterval | null;
targetPlanId: PlanTier;
targetBillingInterval: BillingInterval;
priceUsd: string;
creditsGrantedUsd: string;
introOfferApplied: boolean;
isCurrentPlan: boolean;
isRecommended: boolean;
};
type EnterpriseClickContext = {
currentPlanId: PlanTier | null;
currentBillingInterval: BillingInterval | null;
};
type EnterpriseClickInput =
| (EnterpriseClickContext & { element: 'request_team_access' })
| (EnterpriseClickContext & { element: 'team_lead_submit' });
export type PricingClickInput =
| (PlanCtaClickFields & {
element: 'subscribe_now';
currentPlanId: null;
})
| (PlanCtaClickFields & {
element: 'upgrade_now';
currentPlanId: PlanTier;
})
| ChangeIntervalClickInput
| EnterpriseClickInput;
export type PricingBridgeEvent =
| {
kind: 'plan_exposure';
eventId: string;
eventTime: string;
payload: PlanExposureInput;
}
| {
kind: 'pricing_click';
eventId: string;
eventTime: string;
payload: PricingClickInput;
};
const goTier: PlanTierConfig = {
tier: GO_PLAN.tier,
rank: 0,
recommended: false,
monthly: {
priceUsd: GO_PLAN.monthly.priceUsd,
introPriceUsd: GO_PLAN.monthly.introPriceUsd,
grantUsd: 0,
},
yearly: {
priceUsd: GO_PLAN.yearly.priceUsd,
discountPct: 50,
grantUsd: 0,
},
deployLimit: 0,
};
export const PERSONAL_PRICING_TIERS: readonly PlanTierConfig[] = [
goTier,
...PRICING_SNAPSHOT.tiers,
];
const sourceOverrideKeys = [
'sourceSurface',
'source_surface',
'workspaceTab',
'workspace_tab',
'pricingSource',
'pricing_source',
'source',
'od_entry_source',
] as const;
const sourceByPath: Readonly<Record<string, PricingBridgeSource>> = {
'/wallet': 'wallet',
'/dashboard': 'dashboard',
'/cloud/wallet': 'wallet',
'/cloud/dashboard': 'dashboard',
};
function isTrustedHostedUrl(url: URL): boolean {
return (
url.protocol === 'https:' &&
url.port.length === 0 &&
HOSTED_CLOUD_CONSOLE_DOMAINS.some(
(domain) =>
url.hostname === domain || url.hostname.endsWith(`.${domain}`),
)
);
}
function isTrustedLoopbackUrl(url: URL): boolean {
return (
url.protocol === 'http:' &&
(url.hostname === 'localhost' || url.hostname === '127.0.0.1') &&
url.port.length > 0
);
}
/** Resolve only canonical Vela routes; query state never creates a surface. */
export function resolvePricingBridgeSource(input: {
search: URLSearchParams;
referrer: string;
}): PricingBridgeSource | null {
if (sourceOverrideKeys.some((key) => input.search.has(key))) return null;
if (!input.referrer) return null;
try {
const referrer = new URL(input.referrer);
if (
referrer.href !== input.referrer ||
referrer.username ||
referrer.password ||
referrer.hash ||
(!isTrustedHostedUrl(referrer) && !isTrustedLoopbackUrl(referrer))
) {
return null;
}
return sourceByPath[referrer.pathname] ?? null;
} catch {
return null;
}
}
const idMaxLength = 128;
const maxEventsPerRequest = 8;
const transportTimeoutMs = 3_000;
const usdAmountPattern = /^(?:0|[1-9][0-9]{0,8})\.[0-9]{2}$/u;
// Mirrors Zod 3.25 `z.string().datetime()` used by Vela: real calendar date,
// UTC Z suffix, optional seconds, and arbitrary fractional-second precision.
const velaDateTimePattern = new RegExp(
'^((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d(\\.\\d+)?)?Z$',
'u',
);
function isBoundedId(value: unknown): value is string {
return (
typeof value === 'string' &&
value.length > 0 &&
value.length <= idMaxLength &&
value.trim() === value
);
}
function isPlanTier(value: unknown): value is PlanTier {
return (
value === 'go' ||
value === 'plus' ||
value === 'pro' ||
value === 'max'
);
}
function isBillingInterval(value: unknown): value is BillingInterval {
return value === 'monthly' || value === 'yearly';
}
function isUsdAmount(value: unknown): value is string {
return typeof value === 'string' && usdAmountPattern.test(value);
}
function isBoolean(value: unknown): value is boolean {
return typeof value === 'boolean';
}
function isVelaDateTime(value: unknown): value is string {
return typeof value === 'string' && velaDateTimePattern.test(value);
}
function sanitizedPlanPayload(value: unknown): PlanExposureInput | null {
if (!value || typeof value !== 'object') return null;
const payload = value as Record<string, unknown>;
if (
!isPlanTier(payload.planId) ||
!isBillingInterval(payload.billingInterval) ||
!isUsdAmount(payload.priceUsd) ||
!isUsdAmount(payload.creditsGrantedUsd) ||
!Number.isSafeInteger(payload.deployLimit) ||
Number(payload.deployLimit) < 0 ||
!isBoolean(payload.introOfferApplied) ||
!isBoolean(payload.firstMonthEligible) ||
!isBoolean(payload.isCurrentPlan) ||
!isBoolean(payload.isRecommended)
) {
return null;
}
return {
planId: payload.planId,
billingInterval: payload.billingInterval,
priceUsd: payload.priceUsd,
creditsGrantedUsd: payload.creditsGrantedUsd,
deployLimit: Number(payload.deployLimit),
introOfferApplied: payload.introOfferApplied,
firstMonthEligible: payload.firstMonthEligible,
isCurrentPlan: payload.isCurrentPlan,
isRecommended: payload.isRecommended,
};
}
function sanitizedPlanCtaPayload(
payload: Record<string, unknown>,
): PlanCtaClickFields | null {
if (
(payload.currentBillingInterval !== null &&
!isBillingInterval(payload.currentBillingInterval)) ||
!isPlanTier(payload.targetPlanId) ||
!isBillingInterval(payload.targetBillingInterval) ||
!isUsdAmount(payload.priceUsd) ||
!isUsdAmount(payload.creditsGrantedUsd) ||
!isBoolean(payload.introOfferApplied) ||
!isBoolean(payload.isCurrentPlan) ||
!isBoolean(payload.isRecommended)
) {
return null;
}
return {
currentBillingInterval: payload.currentBillingInterval,
targetPlanId: payload.targetPlanId,
targetBillingInterval: payload.targetBillingInterval,
priceUsd: payload.priceUsd,
creditsGrantedUsd: payload.creditsGrantedUsd,
introOfferApplied: payload.introOfferApplied,
isCurrentPlan: payload.isCurrentPlan,
isRecommended: payload.isRecommended,
};
}
function sanitizedClickPayload(value: unknown): PricingClickInput | null {
if (!value || typeof value !== 'object') return null;
const payload = value as Record<string, unknown>;
switch (payload.element) {
case 'change_interval':
if (
(payload.currentPlanId !== null && !isPlanTier(payload.currentPlanId)) ||
!isBillingInterval(payload.currentBillingInterval) ||
!isBillingInterval(payload.targetBillingInterval)
) {
return null;
}
return {
element: 'change_interval',
currentPlanId: payload.currentPlanId,
currentBillingInterval: payload.currentBillingInterval,
targetBillingInterval: payload.targetBillingInterval,
};
case 'subscribe_now': {
const fields = sanitizedPlanCtaPayload(payload);
if (payload.currentPlanId !== null || !fields) return null;
return { element: 'subscribe_now', currentPlanId: null, ...fields };
}
case 'upgrade_now': {
const fields = sanitizedPlanCtaPayload(payload);
if (!isPlanTier(payload.currentPlanId) || !fields) return null;
return {
element: 'upgrade_now',
currentPlanId: payload.currentPlanId,
...fields,
};
}
case 'request_team_access':
if (
(payload.currentPlanId !== null && !isPlanTier(payload.currentPlanId)) ||
(payload.currentBillingInterval !== null &&
!isBillingInterval(payload.currentBillingInterval))
) {
return null;
}
return {
element: 'request_team_access',
currentPlanId: payload.currentPlanId,
currentBillingInterval: payload.currentBillingInterval,
};
case 'team_lead_submit':
if (
(payload.currentPlanId !== null && !isPlanTier(payload.currentPlanId)) ||
(payload.currentBillingInterval !== null &&
!isBillingInterval(payload.currentBillingInterval))
) {
return null;
}
return {
element: 'team_lead_submit',
currentPlanId: payload.currentPlanId,
currentBillingInterval: payload.currentBillingInterval,
};
default:
return null;
}
}
function sanitizedEvent(event: PricingBridgeEvent): PricingBridgeEvent | null {
if (
!isBoundedId(event.eventId) ||
!isVelaDateTime(event.eventTime)
) {
return null;
}
if (event.kind === 'plan_exposure') {
const payload = sanitizedPlanPayload(event.payload);
return payload
? {
kind: event.kind,
eventId: event.eventId,
eventTime: event.eventTime,
payload,
}
: null;
}
if (event.kind === 'pricing_click') {
const payload = sanitizedClickPayload(event.payload);
return payload
? {
kind: event.kind,
eventId: event.eventId,
eventTime: event.eventTime,
payload,
}
: null;
}
return null;
}
function resolveApiBase(rawValue: string): URL | null {
const value = rawValue.trim();
if (!value) return null;
try {
const url = new URL(value.endsWith('/') ? value : `${value}/`);
if (url.username || url.password || url.search || url.hash) return null;
const hosted = isTrustedHostedUrl(url) && url.pathname.endsWith('/');
const loopback = isTrustedLoopbackUrl(url) && url.pathname === '/';
return hosted || loopback ? url : null;
} catch {
return null;
}
}
/** Best-effort authenticated transport. Invalid input and failures never throw. */
export async function postPricingBridgeEvents(input: {
apiOrigin: string;
sourceSurface: PricingBridgeSource;
sessionId: string;
events: readonly PricingBridgeEvent[];
fetcher?: typeof fetch;
}): Promise<boolean> {
let timeout: ReturnType<typeof setTimeout> | undefined;
try {
const apiBase = resolveApiBase(input.apiOrigin);
if (
!apiBase ||
(input.sourceSurface !== 'wallet' &&
input.sourceSurface !== 'dashboard') ||
!isBoundedId(input.sessionId) ||
!Array.isArray(input.events) ||
input.events.length < 1 ||
input.events.length > maxEventsPerRequest
) {
return false;
}
const events: PricingBridgeEvent[] = [];
const eventIds = new Set<string>();
for (const event of input.events) {
const sanitized = sanitizedEvent(event);
if (!sanitized || eventIds.has(sanitized.eventId)) return false;
eventIds.add(sanitized.eventId);
events.push(sanitized);
}
const abortController = new AbortController();
timeout = setTimeout(
() => abortController.abort(),
transportTimeoutMs,
);
const response = await (input.fetcher ?? fetch)(
new URL('api/v1/analytics/pricing-events', apiBase),
{
method: 'POST',
credentials: 'include',
keepalive: true,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
sourceSurface: input.sourceSurface,
sessionId: input.sessionId,
events,
}),
signal: abortController.signal,
},
);
return response.ok;
} catch {
return false;
} finally {
if (timeout !== undefined) clearTimeout(timeout);
}
}
@@ -0,0 +1,252 @@
import {
PERSONAL_PRICING_TIERS,
postPricingBridgeEvents,
type PlanExposureInput,
type PricingBridgeEvent,
type PricingBridgeSource,
type PricingClickInput,
} from './pricing-analytics-bridge';
import type {
BillingInterval,
PlanTier,
PlanTierConfig,
} from './pricing';
export type ResolvedPricingContext = {
authenticated: true;
sourceSurface: PricingBridgeSource;
currentPlanId: PlanTier | null;
currentBillingInterval: BillingInterval | null;
firstMonthEligible: boolean;
};
type PersonalExposureInput = {
audience: 'creator' | 'team';
interval: BillingInterval;
};
type IntervalChangeInput = Omit<PersonalExposureInput, 'interval'> & {
currentInterval: BillingInterval;
targetInterval: BillingInterval;
userInitiated: boolean;
};
type PlanClickInput = {
audience: 'creator' | 'team';
planId: string;
interval: BillingInterval;
enabled: boolean;
};
type CompatibilityTransport = typeof postPricingBridgeEvents;
type PricingCompatibilityOptions = {
apiOrigin?: string;
sessionId?: string;
tiers?: readonly PlanTierConfig[];
postEvents?: CompatibilityTransport;
now?: () => Date;
createEventId?: () => string;
};
let fallbackEventSequence = 0;
function defaultEventId(): string {
if (typeof globalThis.crypto?.randomUUID === 'function') {
return globalThis.crypto.randomUUID();
}
fallbackEventSequence += 1;
return `pricing-${Date.now()}-${fallbackEventSequence}`;
}
function personalPlanFacts(
tier: PlanTierConfig,
interval: BillingInterval,
firstMonthEligible: boolean,
) {
const introOfferApplied = firstMonthEligible && interval === 'monthly';
const priceUsd = interval === 'monthly'
? introOfferApplied
? tier.monthly.introPriceUsd
: tier.monthly.priceUsd
: tier.yearly.priceUsd;
const creditsGrantedUsd = interval === 'monthly'
? tier.monthly.grantUsd
: tier.yearly.grantUsd / 12;
return {
priceUsd: priceUsd.toFixed(2),
creditsGrantedUsd: creditsGrantedUsd.toFixed(2),
introOfferApplied,
} as const;
}
function recommendedPlan(currentPlanId: PlanTier | null): PlanTier | null {
if (currentPlanId === 'max') return null;
return currentPlanId === 'pro' ? 'max' : 'pro';
}
export function createPricingCompatibilityAnalytics({
apiOrigin = '',
sessionId = '',
tiers = PERSONAL_PRICING_TIERS,
postEvents = postPricingBridgeEvents,
now = () => new Date(),
createEventId = defaultEventId,
}: PricingCompatibilityOptions) {
let context: ResolvedPricingContext | null = null;
let lastExposureSignature: string | null = null;
const createEvent = <T extends PricingBridgeEvent['kind']>(
kind: T,
payload: T extends 'plan_exposure' ? PlanExposureInput : PricingClickInput,
): Extract<PricingBridgeEvent, { kind: T }> => ({
kind,
eventId: createEventId(),
eventTime: now().toISOString(),
payload,
}) as Extract<PricingBridgeEvent, { kind: T }>;
const emit = (bridgeEvents: readonly PricingBridgeEvent[]) => {
if (!context || bridgeEvents.length === 0) return;
try {
void Promise.resolve(postEvents({
apiOrigin,
sourceSurface: context.sourceSurface,
sessionId,
events: bridgeEvents,
})).catch(() => undefined);
} catch {
// Compatibility delivery is best effort and must not block the action.
}
};
const exposureEvents = (
input: PersonalExposureInput,
): PricingBridgeEvent[] => {
const resolved = context;
if (!resolved) return [];
if (input.audience === 'team') {
lastExposureSignature = null;
return [];
}
const signature = JSON.stringify([
input.audience,
input.interval,
resolved.firstMonthEligible,
resolved.currentPlanId,
resolved.currentBillingInterval,
]);
if (lastExposureSignature === signature) return [];
lastExposureSignature = signature;
return tiers.map((tier) => {
const facts = personalPlanFacts(
tier,
input.interval,
resolved.firstMonthEligible,
);
return createEvent('plan_exposure', {
planId: tier.tier,
billingInterval: input.interval,
priceUsd: facts.priceUsd,
creditsGrantedUsd: facts.creditsGrantedUsd,
deployLimit: tier.deployLimit,
introOfferApplied: facts.introOfferApplied,
firstMonthEligible: resolved.firstMonthEligible,
isCurrentPlan:
resolved.currentPlanId === tier.tier &&
resolved.currentBillingInterval === input.interval,
isRecommended: tier.tier === recommendedPlan(resolved.currentPlanId),
});
});
};
const exposePlans = (input: PersonalExposureInput) => {
emit(exposureEvents(input));
};
const changeInterval = (input: IntervalChangeInput) => {
if (!context) return;
const bridgeEvents: PricingBridgeEvent[] = [];
if (
input.audience === 'creator' &&
input.userInitiated &&
input.currentInterval !== input.targetInterval
) {
bridgeEvents.push(createEvent('pricing_click', {
element: 'change_interval',
currentPlanId: context.currentPlanId,
currentBillingInterval: input.currentInterval,
targetBillingInterval: input.targetInterval,
}));
}
bridgeEvents.push(...exposureEvents({
audience: input.audience,
interval: input.targetInterval,
}));
emit(bridgeEvents);
};
const clickPlan = (input: PlanClickInput) => {
if (!context || input.audience !== 'creator' || !input.enabled) return;
const tier = tiers.find((candidate) => candidate.tier === input.planId);
if (!tier) return;
const facts = personalPlanFacts(
tier,
input.interval,
context.firstMonthEligible,
);
const common = {
currentBillingInterval: context.currentBillingInterval,
targetPlanId: tier.tier,
targetBillingInterval: input.interval,
priceUsd: facts.priceUsd,
creditsGrantedUsd: facts.creditsGrantedUsd,
introOfferApplied: facts.introOfferApplied,
isCurrentPlan:
context.currentPlanId === tier.tier &&
context.currentBillingInterval === input.interval,
isRecommended: tier.tier === recommendedPlan(context.currentPlanId),
} as const;
const payload: PricingClickInput = context.currentPlanId === null
? {
element: 'subscribe_now',
currentPlanId: null,
...common,
}
: {
element: 'upgrade_now',
currentPlanId: context.currentPlanId,
...common,
};
emit([createEvent('pricing_click', payload)]);
};
const enterpriseClick = (
element: 'request_team_access' | 'team_lead_submit',
) => {
if (!context) return;
const enterpriseContext = {
currentPlanId: context.currentPlanId,
currentBillingInterval: context.currentBillingInterval,
} as const;
const payload: PricingClickInput = element === 'request_team_access'
? { element: 'request_team_access', ...enterpriseContext }
: { element: 'team_lead_submit', ...enterpriseContext };
emit([createEvent('pricing_click', payload)]);
};
return {
resolveContext(resolved: ResolvedPricingContext) {
if (resolved?.authenticated === true) context = resolved;
},
exposePlans,
changeInterval,
clickPlan,
openEnterpriseLead: () => enterpriseClick('request_team_access'),
submitEnterpriseLead: () => enterpriseClick('team_lead_submit'),
};
}
+1 -1
View File
@@ -10,7 +10,7 @@
* static landing-page contract.
*/
export type PlanTier = 'plus' | 'pro' | 'max';
export type PlanTier = 'go' | 'plus' | 'pro' | 'max';
export type TeamPlanTier =
| 'team_basic'
| 'team_plus'
+130 -1
View File
@@ -747,6 +747,9 @@ const jsonLd = [
});
}
trackCampaignBenefit(audience);
root.dispatchEvent(new CustomEvent('pricing:audience-changed', {
detail: { audience },
}));
};
for (const button of root.querySelectorAll('[data-audience-btn]')) {
button.addEventListener('click', () => {
@@ -789,6 +792,9 @@ const jsonLd = [
// One entry point so user clicks and subscription-state initialization
// share the same rendering path without misreporting initialization as a click.
const activateInterval = (interval, via, shouldTrack = true) => {
const previousInterval = root.getAttribute('data-interval') === 'monthly'
? 'monthly'
: 'yearly';
root.setAttribute('data-interval', interval);
for (const b of root.querySelectorAll('[data-interval-btn]')) {
const on = b.getAttribute('data-interval-btn') === interval;
@@ -797,7 +803,9 @@ const jsonLd = [
}
updateTeamPlan();
syncCtas();
root.dispatchEvent(new CustomEvent('pricing:interval-changed', { detail: { interval } }));
root.dispatchEvent(new CustomEvent('pricing:interval-changed', {
detail: { interval, previousInterval, shouldTrack },
}));
if (shouldTrack) {
track('ui_click', {
area: 'pricing_controls',
@@ -1079,6 +1087,7 @@ const jsonLd = [
for (const btn of document.querySelectorAll('[data-open-lead-modal]')) {
btn.addEventListener('click', () => {
track('team_lead_open', { area: 'enterprise_card' });
root.dispatchEvent(new CustomEvent('pricing:enterprise-open'));
openModal();
});
}
@@ -1167,6 +1176,14 @@ const jsonLd = [
const interval: PersonalBillingInterval = pricingRoot.getAttribute('data-interval') === 'monthly'
? 'monthly'
: 'yearly';
pricingRoot.setAttribute(
'data-current-personal-plan-id',
pricingContext.current?.tier ?? '',
);
pricingRoot.setAttribute(
'data-current-personal-billing-interval',
pricingContext.current?.interval ?? '',
);
for (const cta of pricingRoot.querySelectorAll('[data-pricing-cta]')) {
const tier = cta.getAttribute('data-tier') || '';
if (!personalTiers.has(tier)) continue;
@@ -1243,9 +1260,121 @@ const jsonLd = [
} else {
applyPersonalActions();
}
(pricingRoot as HTMLElement & {
__personalPricingContext?: PersonalPricingContext;
}).__personalPricingContext = context;
pricingRoot.setAttribute('data-personal-pricing-context-resolved', 'true');
pricingRoot.dispatchEvent(new CustomEvent('pricing:personal-context-resolved', {
detail: { authenticated: true, context },
}));
});
}
</script>
<script>
import {
createPricingCompatibilityAnalytics,
} from '../../_lib/pricing-compat-analytics';
import {
resolvePricingBridgeSource,
} from '../../_lib/pricing-analytics-bridge';
import type {
PersonalPricingContext,
} from '../../_lib/pricing-current-plan';
import type { BillingInterval } from '../../_lib/pricing';
const compatibilityRoot = document.querySelector<HTMLElement>('[data-pricing-root]');
const sourceSurface = resolvePricingBridgeSource({
search: new URLSearchParams(window.location.search),
referrer: document.referrer,
});
if (compatibilityRoot && sourceSurface) {
const apiOrigin = compatibilityRoot.getAttribute('data-billing-api-origin') || '';
const sessionId = typeof globalThis.crypto?.randomUUID === 'function'
? globalThis.crypto.randomUUID()
: `pricing-${Date.now()}-${Math.random().toString(36).slice(2)}`;
const compatibility = createPricingCompatibilityAnalytics({
apiOrigin,
sessionId,
});
const readAudience = () =>
compatibilityRoot.getAttribute('data-audience') === 'team'
? 'team' as const
: 'creator' as const;
const readInterval = (): BillingInterval =>
compatibilityRoot.getAttribute('data-interval') === 'monthly'
? 'monthly'
: 'yearly';
const exposeVisiblePlans = () => {
compatibility.exposePlans({
audience: readAudience(),
interval: readInterval(),
});
};
const resolveCompatibilityContext = (context: PersonalPricingContext | null) => {
if (!context) return;
compatibility.resolveContext({
authenticated: true,
sourceSurface,
currentPlanId: context.current?.tier ?? null,
currentBillingInterval: context.current?.interval ?? null,
firstMonthEligible: context.firstMonthIntroEligible,
});
exposeVisiblePlans();
};
compatibilityRoot.addEventListener('pricing:personal-context-resolved', (event) => {
const detail = (event as CustomEvent<{
authenticated?: boolean;
context?: PersonalPricingContext | null;
}>).detail;
if (detail?.authenticated !== true) return;
resolveCompatibilityContext(detail.context ?? null);
});
resolveCompatibilityContext((compatibilityRoot as HTMLElement & {
__personalPricingContext?: PersonalPricingContext;
}).__personalPricingContext ?? null);
compatibilityRoot.addEventListener('pricing:audience-changed', exposeVisiblePlans);
compatibilityRoot.addEventListener('pricing:interval-changed', (event) => {
const detail = (
event as CustomEvent<{
interval?: string;
previousInterval?: string;
shouldTrack?: boolean;
}>
).detail;
const targetInterval = detail?.interval === 'monthly' ? 'monthly' : 'yearly';
const currentInterval = detail?.previousInterval === 'monthly'
? 'monthly'
: 'yearly';
compatibility.changeInterval({
audience: readAudience(),
currentInterval,
targetInterval,
userInitiated: detail?.shouldTrack === true,
});
});
compatibilityRoot.addEventListener('click', (event) => {
const target = event.target;
if (!(target instanceof Element)) return;
const cta = target.closest<HTMLElement>('[data-pricing-cta]');
if (!cta) return;
compatibility.clickPlan({
audience: readAudience(),
planId: cta.getAttribute('data-tier') || '',
interval: readInterval(),
enabled: !cta.hasAttribute('data-subscription-disabled'),
});
});
compatibilityRoot.addEventListener(
'pricing:enterprise-open',
compatibility.openEnterpriseLead,
);
document.addEventListener(
'pricing:enterprise-submit',
compatibility.submitEnterpriseLead,
);
}
</script>
</Layout>
<style>
@@ -0,0 +1,387 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
PERSONAL_PRICING_TIERS,
postPricingBridgeEvents,
resolvePricingBridgeSource,
type PricingBridgeEvent,
} from '../app/_lib/pricing-analytics-bridge.ts';
const eventTime = '2026-08-23T12:00:00.000Z';
const exposureEvent: PricingBridgeEvent = {
kind: 'plan_exposure',
eventId: 'exposure-1',
eventTime,
payload: {
planId: 'go',
billingInterval: 'monthly',
priceUsd: '10.00',
creditsGrantedUsd: '0.00',
deployLimit: 0,
introOfferApplied: false,
firstMonthEligible: false,
isCurrentPlan: false,
isRecommended: false,
},
};
test('personal compatibility catalog contains go plus pro max', () => {
assert.deepEqual(
PERSONAL_PRICING_TIERS.map((tier) => tier.tier),
['go', 'plus', 'pro', 'max'],
);
const go = PERSONAL_PRICING_TIERS[0];
assert.equal(go?.monthly.priceUsd, 10);
assert.equal(go?.yearly.priceUsd, 60);
assert.equal(go?.monthly.grantUsd, 0);
assert.equal(go?.yearly.grantUsd, 0);
assert.equal(go?.deployLimit, 0);
assert.equal(go?.recommended, false);
});
test('source resolver accepts only exact trusted wallet/dashboard routes', () => {
assert.equal(
resolvePricingBridgeSource({
search: new URLSearchParams(),
referrer: 'https://open-design.ai/cloud/dashboard?billing=plan',
}),
'dashboard',
);
assert.equal(
resolvePricingBridgeSource({
search: new URLSearchParams(),
referrer: 'https://open-design.ai/cloud/wallet',
}),
'wallet',
);
assert.equal(
resolvePricingBridgeSource({
search: new URLSearchParams(),
referrer: 'https://vela.powerformer.net/dashboard',
}),
'dashboard',
);
assert.equal(
resolvePricingBridgeSource({
search: new URLSearchParams(),
referrer: 'http://127.0.0.1:5179/wallet',
}),
'wallet',
);
for (const referrer of [
'https://example.com/dashboard',
'https://open-design.ai/cloud/dashboard-settings',
'https://open-design.ai/cloud/wallet/../dashboard',
'https://open-design.ai/cloud/%64ashboard',
'https://open-design.ai.evil.example/cloud/dashboard',
'http://localhost/dashboard',
]) {
assert.equal(
resolvePricingBridgeSource({
search: new URLSearchParams(),
referrer,
}),
null,
referrer,
);
}
});
test('source resolver ignores unrelated handoff state but rejects source overrides', () => {
const unrelated = new URLSearchParams({
od_locale: 'en',
cloud_console_base: 'https://open-design.ai/cloud/',
od_entry_id: 'not-forwarded',
});
assert.equal(
resolvePricingBridgeSource({
search: unrelated,
referrer: 'https://open-design.ai/cloud/dashboard',
}),
'dashboard',
);
for (const [key, value] of [
['sourceSurface', 'dashboard'],
['source_surface', 'wallet'],
['workspaceTab', 'dashboard'],
['pricing_source', 'wallet'],
['source', 'workspace_dashboard'],
['od_entry_source', 'workspace_dashboard'],
['sourceSurface', 'unknown'],
]) {
assert.equal(
resolvePricingBridgeSource({
search: new URLSearchParams([[key, value]]),
referrer: 'https://open-design.ai/cloud/dashboard',
}),
null,
`${key}=${value}`,
);
}
});
test('transport posts only the reduced authenticated bridge body', async () => {
let capturedUrl = '';
let capturedInit: RequestInit | undefined;
const eventWithForbiddenFields = {
...exposureEvent,
registryKey: 'subscription_plan_exposure',
eventName: 'subscription_plan_exposure',
payload: {
...exposureEvent.payload,
planName: 'Go',
autoRechargeSupported: true,
email: 'must-not-leave@example.com',
},
} as PricingBridgeEvent;
const clickEvent: PricingBridgeEvent = {
kind: 'pricing_click',
eventId: 'click-1',
eventTime,
payload: {
element: 'subscribe_now',
currentPlanId: null,
currentBillingInterval: null,
targetPlanId: 'go',
targetBillingInterval: 'monthly',
priceUsd: '10.00',
creditsGrantedUsd: '0.00',
introOfferApplied: false,
isCurrentPlan: false,
isRecommended: false,
},
};
const enterpriseClickEvents: PricingBridgeEvent[] = [
{
kind: 'pricing_click',
eventId: 'enterprise-open-1',
eventTime,
payload: {
element: 'request_team_access',
currentPlanId: null,
currentBillingInterval: null,
},
},
{
kind: 'pricing_click',
eventId: 'enterprise-submit-1',
eventTime,
payload: {
element: 'team_lead_submit',
currentPlanId: 'pro',
currentBillingInterval: 'yearly',
},
},
];
const result = await postPricingBridgeEvents({
apiOrigin: 'https://amr-api.open-design.ai/',
sourceSurface: 'dashboard',
sessionId: 'pricing-session-1',
events: [eventWithForbiddenFields, clickEvent, ...enterpriseClickEvents],
fetcher: async (input, init) => {
capturedUrl = String(input);
capturedInit = init;
return new Response(null, { status: 204 });
},
});
assert.equal(result, true);
assert.equal(
capturedUrl,
'https://amr-api.open-design.ai/api/v1/analytics/pricing-events',
);
assert.equal(capturedInit?.method, 'POST');
assert.equal(capturedInit?.credentials, 'include');
assert.equal(capturedInit?.keepalive, true);
assert.equal(
new Headers(capturedInit?.headers).get('content-type'),
'application/json',
);
assert.ok(capturedInit?.signal instanceof AbortSignal);
assert.deepEqual(JSON.parse(String(capturedInit?.body)), {
sourceSurface: 'dashboard',
sessionId: 'pricing-session-1',
events: [exposureEvent, clickEvent, ...enterpriseClickEvents],
});
});
test('transport mirrors Vela UTC datetime syntax for event times', async () => {
let calls = 0;
const fetcher: typeof fetch = async () => {
calls += 1;
return new Response(null, { status: 204 });
};
for (const accepted of [
'2026-08-23T12:00Z',
'2026-08-23T12:00:00Z',
'2026-08-23T12:00:00.123456Z',
'2028-02-29T23:59:59.9Z',
]) {
assert.equal(
await postPricingBridgeEvents({
apiOrigin: 'https://amr-api.open-design.ai',
sourceSurface: 'dashboard',
sessionId: 'session',
events: [{ ...exposureEvent, eventTime: accepted }],
fetcher,
}),
true,
accepted,
);
}
for (const rejected of [
'2026-08-23',
'2026-08-23T12:00:00+08:00',
'2026-08-23T12:00:00.000+0800',
'2026-08-23T12:00:00z',
'2026-02-30T12:00:00Z',
'2026-08-23T24:00:00Z',
'not-a-time',
]) {
assert.equal(
await postPricingBridgeEvents({
apiOrigin: 'https://amr-api.open-design.ai',
sourceSurface: 'dashboard',
sessionId: 'session',
events: [{ ...exposureEvent, eventTime: rejected }],
fetcher,
}),
false,
rejected,
);
}
assert.equal(calls, 4);
});
test('transport rejects invalid origins and bounded request IDs before fetch', async () => {
let calls = 0;
const fetcher: typeof fetch = async () => {
calls += 1;
return new Response(null, { status: 204 });
};
const attempts = [
{
apiOrigin: 'https://example.com',
sessionId: 'session',
events: [exposureEvent],
},
{
apiOrigin: 'http://localhost',
sessionId: 'session',
events: [exposureEvent],
},
{
apiOrigin: 'http://localhost:5179/not-an-origin/',
sessionId: 'session',
events: [exposureEvent],
},
{
apiOrigin: 'https://open-design.ai/?next=evil',
sessionId: 'session',
events: [exposureEvent],
},
{
apiOrigin: 'https://amr-api.open-design.ai',
sessionId: '',
events: [exposureEvent],
},
{
apiOrigin: 'https://amr-api.open-design.ai',
sessionId: 's'.repeat(129),
events: [exposureEvent],
},
{
apiOrigin: 'https://amr-api.open-design.ai',
sessionId: 'session',
events: [],
},
{
apiOrigin: 'https://amr-api.open-design.ai',
sessionId: 'session',
events: [{ ...exposureEvent, eventId: 'e'.repeat(129) }],
},
{
apiOrigin: 'https://amr-api.open-design.ai',
sessionId: 'session',
events: [exposureEvent, { ...exposureEvent }],
},
] as const;
for (const attempt of attempts) {
assert.equal(
await postPricingBridgeEvents({
...attempt,
sourceSurface: 'dashboard',
fetcher,
}),
false,
);
}
assert.equal(calls, 0);
});
test('transport returns false for malformed runtime shapes without fetching', async () => {
let calls = 0;
const fetcher: typeof fetch = async () => {
calls += 1;
return new Response(null, { status: 204 });
};
const postUnchecked = postPricingBridgeEvents as unknown as (
input: unknown,
) => Promise<boolean>;
const validBase = {
apiOrigin: 'https://amr-api.open-design.ai',
sourceSurface: 'dashboard',
sessionId: 'session',
events: [exposureEvent],
fetcher,
};
for (const malformed of [
null,
undefined,
{},
{ ...validBase, apiOrigin: 42 },
{ ...validBase, events: null },
{ ...validBase, events: { length: 1 } },
{ ...validBase, events: [null] },
{ ...validBase, events: [{ ...exposureEvent, payload: null }] },
{ ...validBase, events: [{ kind: 'plan_exposure' }] },
]) {
assert.equal(await postUnchecked(malformed), false);
}
assert.equal(calls, 0);
});
test('transport fails open on network and endpoint failures', async () => {
assert.equal(
await postPricingBridgeEvents({
apiOrigin: 'https://amr-api.open-design.ai',
sourceSurface: 'wallet',
sessionId: 'session',
events: [exposureEvent],
fetcher: async () => new Response(null, { status: 401 }),
}),
false,
);
assert.equal(
await postPricingBridgeEvents({
apiOrigin: 'https://amr-api.open-design.ai',
sourceSurface: 'wallet',
sessionId: 'session',
events: [exposureEvent],
fetcher: async () => {
throw new TypeError('network unavailable');
},
}),
false,
);
});
@@ -0,0 +1,355 @@
import assert from 'node:assert/strict';
import { spawn, type ChildProcess } from 'node:child_process';
import { existsSync } from 'node:fs';
import { createServer } from 'node:net';
import { after, before, describe, it } from 'node:test';
import { fileURLToPath } from 'node:url';
import { chromium, type Browser, type Page } from 'playwright';
type BridgeEvent = {
kind: 'plan_exposure' | 'pricing_click';
payload: Record<string, unknown>;
};
type BridgeRequest = {
sourceSurface: 'wallet' | 'dashboard';
sessionId: string;
events: BridgeEvent[];
};
type BillingFixture = {
membershipTier?: string;
billingInterval?: 'monthly' | 'yearly';
personalSubscriptionCheckoutAllowed?: boolean;
firstMonthIntroEligible?: boolean;
subscriptionCancelAtPeriodEnd?: boolean;
subscriptionStatus?: string;
subscriptionEntitlementStatus?: string;
availableActions?: string[];
};
const landingRoot = fileURLToPath(new URL('..', import.meta.url));
let browser: Browser;
let server: ChildProcess;
let baseUrl: string;
async function freePort(): Promise<number> {
return new Promise((resolve, reject) => {
const socket = createServer();
socket.once('error', reject);
socket.listen(0, '127.0.0.1', () => {
const address = socket.address();
if (!address || typeof address === 'string') {
socket.close();
reject(new Error('failed to allocate a browser-test port'));
return;
}
const { port } = address;
socket.close((error) => error ? reject(error) : resolve(port));
});
});
}
async function waitForServer(url: string): Promise<void> {
const deadline = Date.now() + 30_000;
while (Date.now() < deadline) {
try {
const response = await fetch(url);
if (response.ok) return;
} catch {
// Astro is still starting.
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
throw new Error(`Astro did not become ready at ${url}`);
}
async function buildLandingPage(): Promise<void> {
await new Promise<void>((resolve, reject) => {
const build = spawn('pnpm', ['exec', 'astro', 'build'], {
cwd: landingRoot,
stdio: ['ignore', 'pipe', 'pipe'],
});
let output = '';
build.stdout?.on('data', (chunk) => { output += String(chunk); });
build.stderr?.on('data', (chunk) => { output += String(chunk); });
build.once('error', reject);
build.once('exit', (code) => {
if (code === 0) resolve();
else reject(new Error(`Astro build failed (${code ?? 'signal'}):\n${output}`));
});
});
}
before(async () => {
const port = await freePort();
baseUrl = `http://127.0.0.1:${port}`;
await buildLandingPage();
server = spawn(
'pnpm',
['exec', 'astro', 'preview', '--host', '127.0.0.1', '--port', String(port)],
{ cwd: landingRoot, stdio: ['ignore', 'pipe', 'pipe'] },
);
let serverOutput = '';
server.stdout?.on('data', (chunk) => { serverOutput += String(chunk); });
server.stderr?.on('data', (chunk) => { serverOutput += String(chunk); });
server.once('exit', (code) => {
if (code && code !== 0) process.stderr.write(serverOutput);
});
await waitForServer(`${baseUrl}/pricing/`);
const localChrome = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
browser = await chromium.launch({
headless: true,
...(existsSync(localChrome) ? { executablePath: localChrome } : {}),
});
});
after(async () => {
await browser?.close();
if (server && !server.killed) server.kill('SIGTERM');
});
async function openPricing(input: {
billing?: BillingFixture;
browserLocale?: string;
sourcePath?: '/dashboard' | '/wallet' | '/not-a-pricing-source' | null;
signedIn?: boolean;
targetHref?: string;
} = {}): Promise<{ page: Page; requests: BridgeRequest[]; navigations: string[] }> {
const context = await browser.newContext({
locale: input.browserLocale ?? 'en-US',
});
const page = await context.newPage();
const requests: BridgeRequest[] = [];
const navigations: string[] = [];
page.on('request', (request) => {
if (request.isNavigationRequest() && request.frame() === page.mainFrame()) {
navigations.push(request.url());
}
});
const sourcePath = input.sourcePath === undefined ? '/dashboard' : input.sourcePath;
const targetHref = input.targetHref ?? '/pricing/';
const signedIn = input.signedIn ?? true;
const billing: BillingFixture = {
personalSubscriptionCheckoutAllowed: true,
firstMonthIntroEligible: true,
subscriptionCancelAtPeriodEnd: false,
availableActions: ['billing_portal'],
...input.billing,
};
await page.route('https://amr-api.open-design.ai/**', async (route) => {
const request = route.request();
const pathname = new URL(request.url()).pathname;
const cors = {
'Access-Control-Allow-Origin': baseUrl,
'Access-Control-Allow-Credentials': 'true',
'Content-Type': 'application/json',
};
if (pathname === '/api/auth/get-session') {
await route.fulfill({
status: 200,
headers: cors,
body: JSON.stringify(signedIn ? { user: { id: 'user-1' } } : null),
});
return;
}
if (pathname === '/api/v1/billing/summary') {
await route.fulfill({ status: 200, headers: cors, body: JSON.stringify(billing) });
return;
}
if (pathname === '/api/v1/analytics/pricing-events') {
requests.push(request.postDataJSON() as BridgeRequest);
await route.fulfill({ status: 204, headers: cors, body: '' });
return;
}
await route.abort();
});
if (sourcePath) {
await page.route(`${baseUrl}${sourcePath}`, async (route) => {
const escapedTargetHref = targetHref
.replaceAll('&', '&amp;')
.replaceAll('"', '&quot;');
await route.fulfill({
status: 200,
contentType: 'text/html',
body: `<!doctype html><a id="pricing-link" href="${escapedTargetHref}">Pricing</a>`,
});
});
await page.goto(`${baseUrl}${sourcePath}`);
await page.locator('#pricing-link').click();
await page.waitForURL((url) => url.pathname.endsWith('/pricing/'));
} else {
await page.goto(`${baseUrl}/pricing/`);
}
return { page, requests, navigations };
}
async function waitForRequests(
requests: BridgeRequest[],
count: number,
): Promise<void> {
await assert.doesNotReject(async () => {
const deadline = Date.now() + 5_000;
while (requests.length < count && Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, 20));
}
assert.ok(requests.length >= count, `expected ${count} bridge request(s), got ${requests.length}`);
});
}
function flattened(requests: BridgeRequest[]): BridgeEvent[] {
return requests.flatMap((request) => request.events);
}
describe('authenticated Pricing compatibility browser wiring', { concurrency: false }, () => {
it('sends corrected Go Plus Pro Max context on the first trusted dashboard exposure', async (t) => {
const { page, requests, navigations } = await openPricing({
billing: {
membershipTier: 'pro',
billingInterval: 'monthly',
firstMonthIntroEligible: false,
},
});
t.after(() => page.context().close());
await page.waitForFunction(() =>
document.querySelector('[data-pricing-root]')?.getAttribute(
'data-personal-pricing-context-resolved',
) === 'true',
);
assert.equal(
await page.evaluate(() => document.referrer),
`${baseUrl}/dashboard`,
navigations.join(' -> '),
);
await waitForRequests(requests, 1);
assert.equal(requests[0]?.sourceSurface, 'dashboard');
assert.ok(requests[0]?.sessionId);
assert.deepEqual(
requests[0]?.events.map((event) => event.payload.planId),
['go', 'plus', 'pro', 'max'],
);
assert.deepEqual(
requests[0]?.events.map((event) => [
event.payload.planId,
event.payload.billingInterval,
event.payload.firstMonthEligible,
event.payload.isCurrentPlan,
]),
[
['go', 'monthly', false, false],
['plus', 'monthly', false, false],
['pro', 'monthly', false, true],
['max', 'monthly', false, false],
],
);
});
it('preserves wallet attribution for a direct Chinese Vela locale handoff', async (t) => {
const targetHref =
'/zh/pricing/?od_locale=zh&cloud_console_base=' +
encodeURIComponent('https://open-design.ai/cloud/');
const { page, requests, navigations } = await openPricing({
browserLocale: 'zh-CN',
sourcePath: '/wallet',
targetHref,
});
t.after(() => page.context().close());
await waitForRequests(requests, 1);
assert.deepEqual(navigations, [
`${baseUrl}/wallet`,
`${baseUrl}${targetHref}`,
]);
assert.equal(
page.url(),
`${baseUrl}/zh/pricing/?cloud_console_base=${encodeURIComponent('https://open-design.ai/cloud/')}`,
);
assert.equal(await page.evaluate(() => document.documentElement.lang), 'zh-CN');
assert.equal(await page.evaluate(() => document.referrer), `${baseUrl}/wallet`);
assert.equal(requests[0]?.sourceSurface, 'wallet');
assert.deepEqual(
requests[0]?.events.map((event) => event.payload.planId),
['go', 'plus', 'pro', 'max'],
);
});
it('fails closed for direct, untrusted-route, and signed-out traffic', async (t) => {
for (const fixture of [
{ sourcePath: null, signedIn: true },
{ sourcePath: '/not-a-pricing-source' as const, signedIn: true },
{ sourcePath: '/dashboard' as const, signedIn: false },
]) {
const opened = await openPricing(fixture);
t.after(() => opened.page.context().close());
await opened.page.waitForTimeout(300);
assert.deepEqual(opened.requests, [], JSON.stringify(fixture));
}
});
it('orders interval click before new exposures and re-exposes after Team', async (t) => {
const { page, requests } = await openPricing();
t.after(() => page.context().close());
await waitForRequests(requests, 1);
await page.locator('[data-interval-btn="monthly"]').click();
await waitForRequests(requests, 2);
assert.deepEqual(
requests[1]?.events.map((event) =>
event.kind === 'pricing_click' ? event.payload.element : event.payload.planId,
),
['change_interval', 'go', 'plus', 'pro', 'max'],
);
await page.locator('[data-audience-btn="team"]').click();
await page.locator('[data-audience-btn="creator"]').click();
await waitForRequests(requests, 3);
assert.deepEqual(
requests[2]?.events.map((event) => event.payload.planId),
['go', 'plus', 'pro', 'max'],
);
});
it('excludes disabled Personal CTAs and records invalid Enterprise submit intent', async (t) => {
const { page, requests } = await openPricing({
billing: { membershipTier: 'pro', billingInterval: 'yearly' },
});
t.after(() => page.context().close());
await waitForRequests(requests, 1);
const disabledPro = page.locator('[data-pricing-cta][data-tier="pro"]');
await assert.doesNotReject(() => disabledPro.click({ force: true }));
await page.waitForTimeout(100);
assert.equal(
flattened(requests).filter((event) => event.kind === 'pricing_click').length,
0,
);
await page.locator('[data-audience-btn="team"]').click();
await page.locator('[data-open-lead-modal]').click();
await waitForRequests(requests, 2);
await page.locator('#ent-form button[type="submit"]').click();
await waitForRequests(requests, 3);
assert.deepEqual(
flattened(requests)
.filter((event) => event.kind === 'pricing_click')
.map((event) => event.payload),
[
{
element: 'request_team_access',
currentPlanId: 'pro',
currentBillingInterval: 'yearly',
},
{
element: 'team_lead_submit',
currentPlanId: 'pro',
currentBillingInterval: 'yearly',
},
],
);
});
});
@@ -0,0 +1,437 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import {
createPricingCompatibilityAnalytics,
type ResolvedPricingContext,
} from '../app/_lib/pricing-compat-analytics.ts';
import {
PERSONAL_PRICING_TIERS,
type PricingBridgeEvent,
type postPricingBridgeEvents,
} from '../app/_lib/pricing-analytics-bridge.ts';
type BridgeRequest = Parameters<typeof postPricingBridgeEvents>[0];
type Harness = ReturnType<typeof harness>;
const dashboardContext: ResolvedPricingContext = {
authenticated: true,
sourceSurface: 'dashboard',
currentPlanId: null,
currentBillingInterval: null,
firstMonthEligible: true,
};
function harness() {
const requests: BridgeRequest[] = [];
let eventSequence = 0;
const analytics = createPricingCompatibilityAnalytics({
apiOrigin: 'https://amr-api.open-design.ai',
sessionId: 'pricing-session-1',
tiers: PERSONAL_PRICING_TIERS,
now: () => new Date('2026-08-23T12:00:00.000Z'),
createEventId: () => `event-${++eventSequence}`,
postEvents: async (request) => {
requests.push(request);
return true;
},
});
return { analytics, requests };
}
function resolve(
testHarness: Harness,
overrides: Partial<Omit<ResolvedPricingContext, 'authenticated'>> = {},
) {
testHarness.analytics.resolveContext({
...dashboardContext,
...overrides,
});
}
function events(testHarness: Harness): PricingBridgeEvent[] {
return testHarness.requests.flatMap((request) => [...request.events]);
}
function exposures(testHarness: Harness) {
return events(testHarness).filter(
(event): event is Extract<PricingBridgeEvent, { kind: 'plan_exposure' }> =>
event.kind === 'plan_exposure',
);
}
function clicks(testHarness: Harness) {
return events(testHarness).filter(
(event): event is Extract<PricingBridgeEvent, { kind: 'pricing_click' }> =>
event.kind === 'pricing_click',
);
}
describe('migrated Pricing compatibility analytics', () => {
it('no-ops every interaction until authenticated Vela context resolves', () => {
const testHarness = harness();
testHarness.analytics.exposePlans({ audience: 'creator', interval: 'yearly' });
testHarness.analytics.changeInterval({
audience: 'creator',
currentInterval: 'yearly',
targetInterval: 'monthly',
userInitiated: true,
});
testHarness.analytics.clickPlan({
audience: 'creator',
planId: 'go',
interval: 'monthly',
enabled: true,
});
testHarness.analytics.openEnterpriseLead();
testHarness.analytics.submitEnterpriseLead();
assert.deepEqual(testHarness.requests, []);
});
it('emits resolved Go Plus Pro Max yearly exposures with literal legacy facts', () => {
const testHarness = harness();
resolve(testHarness);
testHarness.analytics.exposePlans({ audience: 'creator', interval: 'yearly' });
const captured = exposures(testHarness);
assert.deepEqual(
captured.map((event) => event.payload.planId),
['go', 'plus', 'pro', 'max'],
);
assert.deepEqual(captured[0], {
kind: 'plan_exposure',
eventId: 'event-1',
eventTime: '2026-08-23T12:00:00.000Z',
payload: {
planId: 'go',
billingInterval: 'yearly',
priceUsd: '60.00',
creditsGrantedUsd: '0.00',
deployLimit: 0,
introOfferApplied: false,
firstMonthEligible: true,
isCurrentPlan: false,
isRecommended: false,
},
});
assert.deepEqual(
captured.map((event) => [
event.payload.planId,
event.payload.priceUsd,
event.payload.creditsGrantedUsd,
event.payload.deployLimit,
event.payload.isRecommended,
]),
[
['go', '60.00', '0.00', 0, false],
['plus', '168.00', '20.00', 3, false],
['pro', '720.00', '120.00', 20, true],
['max', '1176.00', '300.00', 50, false],
],
);
assert.equal(testHarness.requests[0]?.sourceSurface, 'dashboard');
assert.equal(testHarness.requests[0]?.sessionId, 'pricing-session-1');
});
it('preserves the legacy recommendation progression for every current plan exposure', () => {
const fixtures = [
{ currentPlanId: null, expected: [false, false, true, false] },
{ currentPlanId: 'go', expected: [false, false, true, false] },
{ currentPlanId: 'plus', expected: [false, false, true, false] },
{ currentPlanId: 'pro', expected: [false, false, false, true] },
{ currentPlanId: 'max', expected: [false, false, false, false] },
] as const;
for (const fixture of fixtures) {
const testHarness = harness();
resolve(testHarness, {
currentPlanId: fixture.currentPlanId,
currentBillingInterval: fixture.currentPlanId ? 'monthly' : null,
});
testHarness.analytics.exposePlans({ audience: 'creator', interval: 'yearly' });
assert.deepEqual(
exposures(testHarness).map((event) => event.payload.isRecommended),
fixture.expected,
`current plan ${fixture.currentPlanId ?? 'none'}`,
);
}
});
it('does not let a pre-resolution render swallow the corrected first exposure', () => {
const testHarness = harness();
testHarness.analytics.exposePlans({ audience: 'creator', interval: 'monthly' });
resolve(testHarness, {
sourceSurface: 'wallet',
currentPlanId: 'pro',
currentBillingInterval: 'monthly',
firstMonthEligible: false,
});
testHarness.analytics.exposePlans({ audience: 'creator', interval: 'monthly' });
const captured = exposures(testHarness);
assert.equal(captured.length, 4);
assert.equal(captured[0]?.payload.introOfferApplied, false);
assert.equal(captured[0]?.payload.firstMonthEligible, false);
assert.equal(captured[2]?.payload.planId, 'pro');
assert.equal(captured[2]?.payload.isCurrentPlan, true);
assert.equal(testHarness.requests[0]?.sourceSurface, 'wallet');
});
it('deduplicates the full resolved state and re-exposes after Team', () => {
const testHarness = harness();
resolve(testHarness);
const visible = { audience: 'creator' as const, interval: 'yearly' as const };
testHarness.analytics.exposePlans(visible);
testHarness.analytics.exposePlans(visible);
assert.equal(exposures(testHarness).length, 4);
resolve(testHarness, { firstMonthEligible: false });
testHarness.analytics.exposePlans(visible);
resolve(testHarness, { firstMonthEligible: false, currentPlanId: 'plus' });
testHarness.analytics.exposePlans(visible);
resolve(testHarness, {
firstMonthEligible: false,
currentPlanId: 'plus',
currentBillingInterval: 'monthly',
});
testHarness.analytics.exposePlans(visible);
assert.equal(exposures(testHarness).length, 16);
testHarness.analytics.exposePlans({ audience: 'team', interval: 'yearly' });
testHarness.analytics.exposePlans(visible);
assert.equal(exposures(testHarness).length, 20);
});
it('sends a real interval click before the new interval exposure batch', () => {
const testHarness = harness();
resolve(testHarness);
testHarness.analytics.exposePlans({ audience: 'creator', interval: 'yearly' });
testHarness.requests.length = 0;
testHarness.analytics.changeInterval({
audience: 'creator',
currentInterval: 'yearly',
targetInterval: 'monthly',
userInitiated: true,
});
assert.equal(testHarness.requests.length, 1);
assert.deepEqual(
testHarness.requests[0]?.events.map((event) =>
event.kind === 'pricing_click' ? event.payload.element : event.payload.planId,
),
['change_interval', 'go', 'plus', 'pro', 'max'],
);
assert.deepEqual(clicks(testHarness)[0]?.payload, {
element: 'change_interval',
currentPlanId: null,
currentBillingInterval: 'yearly',
targetBillingInterval: 'monthly',
});
});
it('excludes programmatic interval changes from click events', () => {
const testHarness = harness();
resolve(testHarness);
testHarness.analytics.changeInterval({
audience: 'creator',
currentInterval: 'yearly',
targetInterval: 'monthly',
userInitiated: false,
});
assert.equal(clicks(testHarness).length, 0);
assert.deepEqual(
exposures(testHarness).map((event) => event.payload.planId),
['go', 'plus', 'pro', 'max'],
);
});
it('emits exact subscribe and upgrade payloads for enabled Personal CTAs', () => {
const testHarness = harness();
resolve(testHarness, { firstMonthEligible: true });
testHarness.analytics.clickPlan({
audience: 'creator',
planId: 'go',
interval: 'monthly',
enabled: true,
});
resolve(testHarness, {
currentPlanId: 'plus',
currentBillingInterval: 'monthly',
firstMonthEligible: false,
});
testHarness.analytics.clickPlan({
audience: 'creator',
planId: 'pro',
interval: 'yearly',
enabled: true,
});
assert.deepEqual(clicks(testHarness).map((event) => event.payload), [
{
element: 'subscribe_now',
currentPlanId: null,
currentBillingInterval: null,
targetPlanId: 'go',
targetBillingInterval: 'monthly',
priceUsd: '5.00',
creditsGrantedUsd: '0.00',
introOfferApplied: true,
isCurrentPlan: false,
isRecommended: false,
},
{
element: 'upgrade_now',
currentPlanId: 'plus',
currentBillingInterval: 'monthly',
targetPlanId: 'pro',
targetBillingInterval: 'yearly',
priceUsd: '720.00',
creditsGrantedUsd: '120.00',
introOfferApplied: false,
isCurrentPlan: false,
isRecommended: true,
},
]);
});
it('preserves the legacy recommendation progression for every current plan CTA', () => {
const fixtures = [
{ currentPlanId: null, expected: [false, false, true, false] },
{ currentPlanId: 'go', expected: [false, false, true, false] },
{ currentPlanId: 'plus', expected: [false, false, true, false] },
{ currentPlanId: 'pro', expected: [false, false, false, true] },
{ currentPlanId: 'max', expected: [false, false, false, false] },
] as const;
const planIds = ['go', 'plus', 'pro', 'max'] as const;
for (const fixture of fixtures) {
const testHarness = harness();
resolve(testHarness, {
currentPlanId: fixture.currentPlanId,
currentBillingInterval: fixture.currentPlanId ? 'monthly' : null,
});
for (const planId of planIds) {
testHarness.analytics.clickPlan({
audience: 'creator',
planId,
interval: 'yearly',
enabled: true,
});
}
assert.deepEqual(
clicks(testHarness).map((event) => event.payload.isRecommended),
fixture.expected,
`current plan ${fixture.currentPlanId ?? 'none'}`,
);
}
});
it('excludes disabled, Team, and unknown plan CTAs', () => {
const testHarness = harness();
resolve(testHarness);
for (const input of [
{ audience: 'creator' as const, planId: 'go', enabled: false },
{ audience: 'team' as const, planId: 'go', enabled: true },
{ audience: 'creator' as const, planId: 'team', enabled: true },
{ audience: 'creator' as const, planId: 'unknown', enabled: true },
]) {
testHarness.analytics.clickPlan({ ...input, interval: 'yearly' });
}
assert.deepEqual(testHarness.requests, []);
});
it('fails closed when a Personal CTA omits its audience', () => {
const testHarness = harness();
resolve(testHarness);
testHarness.analytics.clickPlan({
planId: 'go',
interval: 'monthly',
enabled: true,
});
assert.deepEqual(testHarness.requests, []);
});
it('records Enterprise submit as an immediate intent event', () => {
const testHarness = harness();
resolve(testHarness, {
currentPlanId: 'pro',
currentBillingInterval: 'yearly',
});
testHarness.analytics.openEnterpriseLead();
testHarness.analytics.submitEnterpriseLead();
assert.deepEqual(clicks(testHarness).map((event) => event.payload), [
{
element: 'request_team_access',
currentPlanId: 'pro',
currentBillingInterval: 'yearly',
},
{
element: 'team_lead_submit',
currentPlanId: 'pro',
currentBillingInterval: 'yearly',
},
]);
});
it('records nullable Enterprise context for users without a current plan', () => {
const testHarness = harness();
resolve(testHarness);
testHarness.analytics.openEnterpriseLead();
testHarness.analytics.submitEnterpriseLead();
assert.deepEqual(clicks(testHarness).map((event) => event.payload), [
{
element: 'request_team_access',
currentPlanId: null,
currentBillingInterval: null,
},
{
element: 'team_lead_submit',
currentPlanId: null,
currentBillingInterval: null,
},
]);
});
it('keeps transport failures best effort', async () => {
const rejected = createPricingCompatibilityAnalytics({
apiOrigin: 'https://amr-api.open-design.ai',
sessionId: 'pricing-session-1',
tiers: PERSONAL_PRICING_TIERS,
postEvents: async () => {
throw new Error('offline');
},
});
const synchronous = createPricingCompatibilityAnalytics({
apiOrigin: 'https://amr-api.open-design.ai',
sessionId: 'pricing-session-2',
tiers: PERSONAL_PRICING_TIERS,
postEvents: (() => {
throw new Error('offline');
}) as typeof postPricingBridgeEvents,
});
for (const analytics of [rejected, synchronous]) {
analytics.resolveContext(dashboardContext);
assert.doesNotThrow(() => analytics.openEnterpriseLead());
}
await new Promise((resolvePromise) => setTimeout(resolvePromise, 0));
});
});
@@ -1012,6 +1012,11 @@ describe("pricing contract", () => {
assert.match(page, /data-downgrade-plan-label=\{planActionLabels\.downgrade\}/);
assert.match(page, /data-upgrade-plan-label=\{planActionLabels\.upgrade\}/);
assert.match(page, /loadPersonalPricingContext\(apiOrigin\)/);
assert.match(page, /pricing:personal-context-resolved/);
assert.match(page, /resolvePricingBridgeSource/);
assert.match(page, /authenticated:\s*true/);
assert.doesNotMatch(page, /pricingCompatibilityAttribution/);
assert.doesNotMatch(page, /tiers:\s*PRICING_SNAPSHOT\.tiers/);
assert.match(page, /resolvePersonalPlanAction\(pricingContext/);
assert.match(page, /action\.kind === 'dual_change'/);
assert.doesNotMatch(page, /action\.kind === 'manage_billing'/);
@@ -1034,6 +1039,31 @@ describe("pricing contract", () => {
assert.match(individualPlans, /\.pricing-card-cta\s*\{[^}]*border:\s*0;/s);
});
it("records Pricing Enterprise submit intent before shared-form validation", async () => {
const [page, form] = await Promise.all([
readFile(PRICING_PAGE_PATH, "utf8"),
readFile(
new URL("../app/_components/enterprise-lead-form.astro", import.meta.url),
"utf8",
),
]);
const submitHandler = form.slice(
form.indexOf("form.addEventListener('submit'"),
form.indexOf("const data = new FormData(form)"),
);
assert.match(
submitHandler,
/pricing:enterprise-submit[\s\S]*?\['email', 'team-size'/,
);
assert.doesNotMatch(
page.slice(
page.indexOf("modal.addEventListener('od:lead-success'"),
page.indexOf("});", page.indexOf("modal.addEventListener('od:lead-success'")) + 3,
),
/pricing:enterprise-submit/,
);
});
it("restores account actions only on Pricing", async () => {
const layout = await readFile(
new URL("../app/_components/sub-page-layout.astro", import.meta.url),
@@ -0,0 +1,250 @@
# OpenDesign Authenticated Pricing Analytics Emitter 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:** Send migrated Pricing interactions to Vela's authenticated compatibility endpoint so the existing AMR subscription funnel resumes without changing the Pricing or checkout flow.
**Architecture:** A pure controller builds reduced, strictly bounded bridge records and a transport posts them to Vela with credentials and keepalive. The page activates the controller only after authenticated pricing context and a trusted wallet/dashboard source resolve; existing OpenDesign PostHog events remain independent.
**Tech Stack:** Astro 6, TypeScript, Node test runner, Playwright, PostHog wrapper (unchanged)
## Global Constraints
- Base the work on the latest `nexu-io/open-design/main`.
- Do not change Pricing UI, prices, entitlements, CTA destinations, checkout behavior, or existing OpenDesign analytics.
- Only authenticated Vela sessions arriving from trusted wallet/dashboard surfaces enter the compatibility funnel.
- Compatibility delivery is best effort and must never block navigation or form submission.
- Never include email, company, lead free text, raw URL, or raw referrer in the bridge request.
- Vela endpoint must deploy before this emitter.
---
### Task 1: Define the reduced bridge contract and strict source resolver
**Files:**
- Modify: `apps/landing-page/app/_lib/pricing.ts`
- Create: `apps/landing-page/app/_lib/pricing-analytics-bridge.ts`
- Create: `apps/landing-page/tests/pricing-analytics-bridge.test.ts`
**Interfaces:**
- Consumes: `GO_PLAN`, `PRICING_SNAPSHOT.tiers`, `HOSTED_CLOUD_CONSOLE_DOMAINS`
- Produces: `PERSONAL_PRICING_TIERS`, `resolvePricingBridgeSource()`, `postPricingBridgeEvents()`, and the reduced `PricingBridgeEvent` union
- [ ] **Step 1: Write failing contract tests**
```ts
test('personal compatibility catalog contains go plus pro max', () => {
assert.deepEqual(PERSONAL_PRICING_TIERS.map((tier) => tier.tier), [
'go', 'plus', 'pro', 'max',
]);
});
test('source resolver accepts only exact trusted wallet/dashboard routes', () => {
assert.equal(resolvePricingBridgeSource({
search: new URLSearchParams(),
referrer: 'https://open-design.ai/cloud/dashboard?billing=plan',
}), 'dashboard');
assert.equal(resolvePricingBridgeSource({
search: new URLSearchParams(),
referrer: 'https://example.com/dashboard',
}), null);
});
```
Also assert that unknown query values, substring routes, oversized IDs, and untrusted hosts return `null`, and that the Go adapter yields price 10/60, credits 0, deploy limit 0, and `recommended=false`.
- [ ] **Step 2: Run the focused tests and verify RED**
Run: `pnpm --filter @open-design/landing-page exec node --import tsx --test tests/pricing-analytics-bridge.test.ts`
Expected: FAIL because the catalog, resolver, and transport do not exist.
- [ ] **Step 3: Implement the minimal bridge module**
```ts
export type PricingBridgeSource = 'wallet' | 'dashboard';
export type PricingBridgeEvent =
| { kind: 'plan_exposure'; eventId: string; eventTime: string; payload: PlanExposureInput }
| { kind: 'pricing_click'; eventId: string; eventTime: string; payload: PricingClickInput };
export async function postPricingBridgeEvents(input: {
apiOrigin: string;
sourceSurface: PricingBridgeSource;
sessionId: string;
events: readonly PricingBridgeEvent[];
fetcher?: typeof fetch;
}): Promise<boolean>;
```
Build `PERSONAL_PRICING_TIERS` by adapting `GO_PLAN` and appending the existing snapshot tiers. Validate the API origin with the same hosted/loopback policy used by the checkout handoff. POST only the reduced body to `/api/v1/analytics/pricing-events` with `credentials: 'include'`, `keepalive: true`, JSON headers, and a short abort timeout. Return `false` on every failure without throwing.
- [ ] **Step 4: Run the focused tests and verify GREEN**
Run: `pnpm --filter @open-design/landing-page exec node --import tsx --test tests/pricing-analytics-bridge.test.ts`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add apps/landing-page/app/_lib/pricing.ts \
apps/landing-page/app/_lib/pricing-analytics-bridge.ts \
apps/landing-page/tests/pricing-analytics-bridge.test.ts
git commit -m "feat(analytics): add authenticated pricing bridge contract"
```
### Task 2: Rebuild compatibility semantics around resolved authentication context
**Files:**
- Modify: `apps/landing-page/app/_lib/pricing-compat-analytics.ts`
- Modify: `apps/landing-page/tests/pricing-compat-analytics.test.ts`
**Interfaces:**
- Consumes: `PricingBridgeEvent`, `PERSONAL_PRICING_TIERS`
- Produces: `createPricingCompatibilityAnalytics()` that emits reduced records only after `resolveContext()`
- [ ] **Step 1: Replace the controller assertions with failing legacy-semantics tests**
Add literal expectations proving:
```ts
assert.deepEqual(exposures.map((event) => event.payload.planId), [
'go', 'plus', 'pro', 'max',
]);
assert.equal(exposures[0].payload.creditsGrantedUsd, '0.00');
assert.equal(exposures[0].payload.deployLimit, 0);
```
Add independent tests that no exposure occurs before context resolution, a same-interval context correction emits the correct first exposure, dedupe includes eligibility/current-plan state, interval click precedes new exposures, disabled/Team CTAs are excluded, and Enterprise submit is an intent event.
- [ ] **Step 2: Run the controller test and verify RED**
Run: `pnpm --filter @open-design/landing-page exec node --import tsx --test tests/pricing-compat-analytics.test.ts`
Expected: FAIL for missing Go, eager exposure, incomplete signature, and old transport event names.
- [ ] **Step 3: Implement the minimal state machine**
The controller owns:
```ts
type ResolvedPricingContext = {
authenticated: true;
sourceSurface: 'wallet' | 'dashboard';
currentPlanId: 'go' | 'plus' | 'pro' | 'max' | null;
currentBillingInterval: 'monthly' | 'yearly' | null;
firstMonthEligible: boolean;
};
```
Before `resolveContext()`, all methods no-op. After resolution, exposure signatures include audience, interval, eligibility, current plan, and current interval. Emit arrays through the bridge transport rather than `window.__odTrack`. Preserve exact old click intent and ordering.
- [ ] **Step 4: Run the focused controller and bridge suites**
Run: `pnpm --filter @open-design/landing-page exec node --import tsx --test tests/pricing-compat-analytics.test.ts tests/pricing-analytics-bridge.test.ts`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add apps/landing-page/app/_lib/pricing-compat-analytics.ts \
apps/landing-page/tests/pricing-compat-analytics.test.ts
git commit -m "fix(analytics): restore complete personal pricing semantics"
```
### Task 3: Wire Pricing context, DOM interactions, and navigation-safe delivery
**Files:**
- Modify: `apps/landing-page/app/pages/pricing/index.astro`
- Modify: `apps/landing-page/app/_components/enterprise-lead-form.astro`
- Modify: `apps/landing-page/tests/pricing-contract.test.ts`
- Create: `apps/landing-page/tests/pricing-analytics-browser.test.ts`
**Interfaces:**
- Consumes: Vela `POST /api/v1/analytics/pricing-events`
- Produces: real Pricing-page bridge requests after authenticated context resolution
- [ ] **Step 1: Write failing DOM/browser tests**
Use a real browser page with intercepted session, billing-summary, and pricing-events requests. Assert:
```ts
assert.deepEqual(
captured.events.filter((event) => event.kind === 'plan_exposure')
.map((event) => event.payload.planId),
['go', 'plus', 'pro', 'max'],
);
assert.equal(captured.sourceSurface, 'dashboard');
```
Also assert no request for signed-out/direct/untrusted traffic; resolved current-plan fields on first exposure; interval click before exposure batch; Team-to-Personal re-exposure; disabled CTA exclusion; and `team_lead_submit` on submit intent even when validation fails.
- [ ] **Step 2: Run the browser test and verify RED**
Run: `pnpm --filter @open-design/landing-page exec node --import tsx --test tests/pricing-analytics-browser.test.ts`
Expected: FAIL because the page still captures compatibility events locally and emits before context resolution.
- [ ] **Step 3: Wire the context-resolved event**
After `loadPersonalPricingContext(apiOrigin)` resolves, set a resolved marker and dispatch:
```ts
pricingRoot.dispatchEvent(new CustomEvent('pricing:personal-context-resolved', {
detail: { authenticated: context !== null, context },
}));
```
The compatibility script waits for this event, resolves the trusted source, creates one session ID, and sends bridge batches. Do not initialize for null context or null source.
- [ ] **Step 4: Restore Enterprise submit intent timing**
Dispatch `pricing:enterprise-submit` from the form's synchronous `submit` event before validation. Remove the compatibility dispatch from `od:lead-success`; retain the existing OpenDesign success bridge and non-PII lead analytics unchanged.
- [ ] **Step 5: Run browser, contract, and full landing tests**
Run:
```bash
pnpm --filter @open-design/landing-page exec node --import tsx --test \
tests/pricing-analytics-browser.test.ts tests/pricing-contract.test.ts
pnpm --filter @open-design/landing-page test
```
Expected: all tests pass.
- [ ] **Step 6: Commit**
```bash
git add apps/landing-page/app/pages/pricing/index.astro \
apps/landing-page/app/_components/enterprise-lead-form.astro \
apps/landing-page/tests/pricing-contract.test.ts \
apps/landing-page/tests/pricing-analytics-browser.test.ts
git commit -m "fix(analytics): relay authenticated pricing funnel events"
```
### Task 4: Validate, document dependency, and update the OpenDesign PR
**Files:**
- Modify: `docs/superpowers/specs/2026-08-23-restore-pricing-plan-exposure-design.md` only if implementation reveals a factual mismatch
- Modify: PR #7299 body through `odc`
- [ ] **Step 1: Run all static and build gates**
```bash
pnpm --filter @open-design/landing-page typecheck
pnpm --filter @open-design/landing-page build
pnpm guard
git diff --check
```
Expected: zero errors and a clean worktree after committed changes.
- [ ] **Step 2: Verify against a locally running Vela endpoint**
With an authenticated fixture, trigger initial Personal exposure, interval change, Personal/Team return, plan CTA, Enterprise open, and invalid Enterprise submit. Confirm requests reach Vela and no compatibility events hit `window.__odTrack`.
- [ ] **Step 3: Update PR #7299 through `odc`**
Cross-link the Vela PR, state that Vela must deploy first, list exact restored interactions, and remove the obsolete claim that direct OpenDesign PostHog capture restores AMR.
- [ ] **Step 4: Request independent review**
Review the final `origin/main..HEAD` diff using the requesting-code-review skill. Resolve every Critical and Important issue before considering the PR ready.
@@ -0,0 +1,251 @@
# Vela Authenticated Pricing Analytics Endpoint 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 a narrow authenticated Vela endpoint that maps migrated OpenDesign Pricing interactions into the existing AMR subscription analytics registry.
**Architecture:** A dedicated module owns a strict reduced request schema, source-origin checks, bounded per-user throttling, and mapping into `AnalyticsService`. The browser never supplies registry keys or Vela common metadata; the server stamps them and reuses the existing AMR/PostHog pipeline.
**Tech Stack:** Hono, Zod, TypeScript, Vitest, Vela AnalyticsService/PostHog repository
## Global Constraints
- Base the work on the latest `powerformer/vela/main`.
- Preserve the existing registry keys, AMR PostHog event names, payload schemas, dashboards, and alerts.
- Require a valid Vela user session; never make either legacy event anonymous/public-page allowed.
- Accept only trusted configured web origins and no PII/free-form payload fields.
- The endpoint is best effort for callers but strict on auth, body size, timestamp freshness, enums, batch size, and rate.
- No database schema or billing behavior changes.
---
### Task 1: Define and test the reduced Pricing bridge contract
**Files:**
- Create: `services/api/src/pricing-analytics.ts`
- Create: `services/api/test/pricing-analytics.test.ts`
**Interfaces:**
- Consumes: `AnalyticsCommonEvent`, `AnalyticsService`, `analyticsEventRegistry`
- Produces: `pricingAnalyticsRequestSchema`, `mapPricingAnalyticsRequest()`, `registerPricingAnalyticsRoute()`
- [ ] **Step 1: Write failing schema tests**
```ts
expect(pricingAnalyticsRequestSchema.safeParse({
sourceSurface: 'dashboard',
sessionId: 'pricing-session-1',
events: [{
kind: 'plan_exposure',
eventId: 'pricing-event-1',
eventTime: '2026-08-23T12:00:00.000Z',
payload: {
planId: 'go',
billingInterval: 'monthly',
priceUsd: '5.00',
creditsGrantedUsd: '0.00',
deployLimit: 0,
introOfferApplied: true,
firstMonthEligible: true,
isCurrentPlan: false,
isRecommended: false,
},
}],
}).success).toBe(true);
```
Add negative cases for arbitrary registry keys, unknown properties, unknown plans/elements, raw URL/referrer/email fields, more than eight events, event IDs over 128 characters, malformed times, and source surfaces outside wallet/dashboard.
- [ ] **Step 2: Run the focused test and verify RED**
Run: `pnpm --filter @vela/api exec vitest run test/pricing-analytics.test.ts`
Expected: FAIL because the bridge module does not exist.
- [ ] **Step 3: Implement strict schemas**
Use `.strict()` at the request, event, and payload layers. Export a discriminated union:
```ts
type PricingAnalyticsInput =
| { kind: 'plan_exposure'; eventId: string; eventTime: string; payload: PlanPayload }
| { kind: 'pricing_click'; eventId: string; eventTime: string; payload: ClickPayload };
```
Allow only plan IDs `go|plus|pro|max`, intervals `monthly|yearly`, and click elements `change_interval|subscribe_now|upgrade_now|request_team_access|team_lead_submit`. Limit a request to 18 events and body size to 16 KiB.
- [ ] **Step 4: Run schema tests and verify GREEN**
Run: `pnpm --filter @vela/api exec vitest run test/pricing-analytics.test.ts`
Expected: schema cases pass.
- [ ] **Step 5: Commit**
```bash
git add services/api/src/pricing-analytics.ts services/api/test/pricing-analytics.test.ts
git commit -m "feat(analytics): define authenticated pricing bridge contract"
```
### Task 2: Map reduced inputs to exact legacy analytics records
**Files:**
- Modify: `services/api/src/pricing-analytics.ts`
- Modify: `services/api/test/pricing-analytics.test.ts`
**Interfaces:**
- Consumes: validated bridge request, authenticated profile, server request metadata
- Produces: `Array<{ common: AnalyticsCommonEvent; payload: unknown }>` accepted by `AnalyticsService.ingest()`
- [ ] **Step 1: Write failing literal mapping tests**
Assert that plan exposure maps to:
```ts
expect(mapped[0]).toMatchObject({
common: {
registryKey: 'subscription_plan_exposure',
eventName: 'subscription_plan_exposure',
eventType: 'view',
platform: 'web',
},
payload: {
pageName: 'workspace',
workspaceTab: 'dashboard',
area: 'subscription_pricing',
entryPoint: 'open_design_entry',
planId: 'go',
},
});
```
Assert that pricing click maps to `registryKey=subscription_pricing_click`, `eventName=ui_click`, `eventType=click`. Include wallet/dashboard, interval, plan CTA, Enterprise elements, zero-valued Go fields, null current-plan fields, and strict validated OpenDesign attribution.
- [ ] **Step 2: Run the mapping test and verify RED**
Run: `pnpm --filter @vela/api exec vitest run test/pricing-analytics.test.ts`
Expected: FAIL because the mapper is missing.
- [ ] **Step 3: Implement the mapper**
Derive event name/type from `analyticsEventRegistry`, never from browser input. Stamp required nullable common fields with server-owned values, use the authenticated profile separately in `AnalyticsService.ingest`, derive locale from bounded request input or `Accept-Language`, and browser from `User-Agent`. Reject event times older than 24 hours or more than five minutes in the future before mapping.
- [ ] **Step 4: Validate mapped payloads through the real registry**
Call the same registry payload validator used by `AnalyticsService` in the test path. Mutation checks must fail for `pageName=pricing`, a missing click `workspaceTab`, an unknown element, or an invalid attribution enum.
- [ ] **Step 5: Run focused tests and commit**
```bash
pnpm --filter @vela/api exec vitest run test/pricing-analytics.test.ts
git add services/api/src/pricing-analytics.ts services/api/test/pricing-analytics.test.ts
git commit -m "feat(analytics): map pricing bridge to legacy registry"
```
### Task 3: Register the authenticated, origin-restricted, bounded route
**Files:**
- Modify: `services/api/src/pricing-analytics.ts`
- Modify: `services/api/src/app.ts`
- Create: `services/api/test/pricing-analytics-routes.test.ts`
**Interfaces:**
- Consumes: `getApiProfile(headers)`, `trustedWebOrigins(config)`, `AnalyticsService`
- Produces: `POST /api/v1/analytics/pricing-events` returning 202/400/401/403/413/429
- [ ] **Step 1: Write failing route tests**
Build a focused Hono test app with an in-memory analytics repository. Prove:
- missing session returns 401;
- untrusted or missing browser Origin returns 403;
- oversized body returns 413 before JSON parsing;
- invalid strict payload returns 400 without issue values containing request data;
- more than 120 accepted events per authenticated user per minute returns 429;
- a valid request returns 202 and stores exact mapped records;
- a duplicate event ID is forwarded as the same PostHog `$insert_id`, preserving PostHog ingestion deduplication without claiming repository-level deduplication.
- [ ] **Step 2: Run route tests and verify RED**
Run: `pnpm --filter @vela/api exec vitest run test/pricing-analytics-routes.test.ts`
Expected: FAIL because the route is not registered.
- [ ] **Step 3: Implement route safeguards**
Register the route only when analytics is configured. Resolve the profile before body mapping. Compare the normalized `Origin` header against `trustedWebOrigins(config)`. Read at most 16 KiB using the existing bounded-body helper pattern. Add an injected per-user fixed-window limiter with 120 events/minute and periodic stale-entry pruning; production creates one limiter with the app, tests inject a deterministic clock.
- [ ] **Step 4: Ingest through the existing AnalyticsService**
```ts
await analytics.ingest({
profile,
events: mapPricingAnalyticsRequest(parsed.data, requestMetadata),
});
return c.json({ accepted: parsed.data.events.length }, 202);
```
Do not alter `anonymousAllowed` or `publicPageAllowed` on either registry entry. Preserve the browser-supplied `eventId` as the analytics record `eventId`; the existing PostHog adapter maps it to `$insert_id`, so best-effort retries keep a stable ingestion identity.
- [ ] **Step 5: Run focused route and existing analytics tests**
```bash
pnpm --filter @vela/api exec vitest run \
test/pricing-analytics.test.ts \
test/pricing-analytics-routes.test.ts \
test/analytics-events.test.ts
```
Expected: all pass.
- [ ] **Step 6: Commit**
```bash
git add services/api/src/pricing-analytics.ts services/api/src/app.ts \
services/api/test/pricing-analytics-routes.test.ts
git commit -m "feat(api): accept authenticated pricing analytics"
```
### Task 4: Prove AMR PostHog compatibility and publish the Vela PR
**Files:**
- Modify: `services/api/test/analytics-events.test.ts`
- Create: `docs/superpowers/specs/2026-08-23-pricing-analytics-bridge-design.md`
- Create: `docs/superpowers/plans/2026-08-23-pricing-analytics-bridge.md`
**Interfaces:**
- Consumes: final mapped records
- Produces: regression proof for exact PostHog event names/properties and a ready Vela PR
- [ ] **Step 1: Add a failing PostHog delivery regression**
Pass mapped plan and click records through `PostHogAnalyticsRepository` and assert:
```ts
expect(batch[0].event).toBe('subscription_plan_exposure');
expect(batch[0].properties.registry_key).toBe('subscription_plan_exposure');
expect(batch[1].event).toBe('ui_click');
expect(batch[1].properties.registry_key).toBe('subscription_pricing_click');
```
- [ ] **Step 2: Run the regression and verify it passes through production mapping**
Run: `pnpm --filter @vela/api exec vitest run test/analytics-events.test.ts test/pricing-analytics.test.ts`
Expected: PASS after the route mapper is complete.
- [ ] **Step 3: Run repository verification**
```bash
pnpm --filter @vela/shared build
pnpm --filter @vela/api typecheck
pnpm --filter @vela/api test
pnpm lint
git diff --check
```
Expected: zero failures.
- [ ] **Step 4: Create the Vela PR with `odc`**
Verify `nexus status --json`, `odc whoami`, and `odc agent verify codex --scope project`. Create the PR with Vela-first deployment instructions and cross-link OpenDesign PR #7299.
- [ ] **Step 5: Request independent review**
Review `origin/main..HEAD` with the requesting-code-review skill. Resolve every Critical and Important issue, rerun validation, and update the PR before handing the pair to the user.
@@ -0,0 +1,215 @@
# Restore Authenticated Pricing Funnel Analytics Design
## Goal
Restore the Vela subscription funnel that was lost when the authenticated
plan-selection modal was replaced by the shared OpenDesign `/pricing/` page.
The restored events must continue into the existing AMR PostHog project and
preserve the current dashboard and alert contract. The migrated Pricing UI,
checkout handoff, prices, and entitlements remain unchanged.
## Confirmed Legacy Semantics
The retired surface lived behind Vela authentication on `/wallet` and
`/dashboard`. It emitted `subscription_plan_exposure` for Go, Plus, Pro, and
Max, and emitted the registry-backed `subscription_pricing_click` event for
plan CTAs, interval changes, and the Enterprise lead interactions that existed
on the modal. Both registry entries have `anonymousAllowed: false` and
`publicPageAllowed: false`.
The Vela API stored the events in AMR PostHog as:
| Registry key | AMR PostHog event | Existing breakdown key |
| --- | --- | --- |
| `subscription_plan_exposure` | `subscription_plan_exposure` | `registry_key=subscription_plan_exposure` |
| `subscription_pricing_click` | `ui_click` | `registry_key=subscription_pricing_click` |
The public Pricing page's direct OpenDesign PostHog capture is a different
pipeline. Reusing the same JavaScript event name there does not restore the AMR
funnel. The new bridge therefore terminates at Vela's authenticated analytics
pipeline rather than OpenDesign PostHog.
## Scope
- Two repositories and two PRs: Vela provides the authenticated ingestion
boundary; OpenDesign emits from the migrated Pricing interactions.
- Only visitors with a valid Vela session and a trusted `/wallet` or
`/dashboard` Pricing entry are included in the restored AMR funnel.
- Anonymous or direct public Pricing visitors continue to use the existing
OpenDesign Pricing analytics only.
- Restore equivalent interactions that still exist: Go/Plus/Pro/Max exposure,
enabled Personal subscribe/upgrade CTA, interval change, Enterprise lead
open, and Enterprise lead submit intent.
- Do not recreate the retired modal, removed controls, checkout-result events,
or PII-bearing legacy lead payloads.
- Do not change Pricing rendering, plan selection, checkout URLs, payment
behavior, entitlement behavior, or existing OpenDesign analytics.
## Architecture
### Vela: authenticated Pricing analytics endpoint
Add a purpose-built authenticated endpoint at
`POST /api/v1/analytics/pricing-events`. The endpoint accepts only a small,
discriminated Pricing request contract; it does not accept arbitrary registry
keys, event names, common metadata, or free-form properties.
The endpoint:
1. requires the same valid Vela user session used by the public Pricing page's
existing subscription-context request;
2. validates the request origin against the existing OpenDesign hosted-origin
policy and rejects anonymous calls with `401`;
3. accepts only `plan_exposure` and `pricing_click` records, with a bounded
batch size and bounded string fields;
4. accepts only `sourceSurface=wallet|dashboard`, Personal plan IDs
`go|plus|pro|max`, billing intervals `monthly|yearly`, and the legacy click
elements that the new page actually emits;
5. maps the reduced request into the existing analytics registry contract,
stamping `pageName=workspace`, `workspaceTab=sourceSurface`, the authenticated
user profile, server receive time, registry key, event name, event type, and
normal common metadata;
6. sends the mapped records through the existing `AnalyticsService`, so the
primary AMR destination receives the same event names and
`registry_key` properties as before;
7. never accepts or forwards email, name, company, free-form lead text, URL,
referrer text, or other PII.
The client supplies a bounded event ID and occurrence time so navigation-safe
`keepalive` delivery remains idempotent. The server rejects stale, future, or
malformed timestamps and validates every plan/click payload against the
existing registry schema before storage.
### OpenDesign: Pricing compatibility emitter
The Pricing page gains a small client that posts reduced events to the Vela
endpoint with credentials and `keepalive: true`. It does not synthesize Vela's
internal analytics envelope and does not send these compatibility records to
`window.__odTrack`.
The emitter activates only after all of the following are true:
- the Cloud Console destination is an allowlisted production Vela origin;
- the subscription-context request confirms a valid authenticated session;
- the inbound entry resolves to a trusted Vela `/wallet` or `/dashboard`
surface;
- the Personal pricing context has finished resolving, including current plan,
current interval, and first-month eligibility.
The trusted surface is derived only from the browser referrer when it has an
allowlisted OpenDesign/Vela origin and an exact `/wallet`, `/dashboard`,
`/cloud/wallet`, or `/cloud/dashboard` path. Query parameters never select the
source surface, and arbitrary query strings or external referrers cannot create
new analytics dimensions.
Vela links directly to the matching localized Pricing route and carries
`od_locale` alongside `cloud_console_base`. Avoiding an intermediate locale
redirect preserves the trusted Vela referrer needed by this source check; the
locale parameter itself is not used as analytics attribution.
## Event Mapping
### Plan exposure
When the Personal audience is visible, emit one `plan_exposure` input for each
visible Personal plan: Go, Plus, Pro, and Max. Vela maps each record to
`subscription_plan_exposure` with the legacy properties:
- `pageName=workspace`
- `workspaceTab=wallet|dashboard`
- `area=subscription_pricing`
- `entryPoint=open_design_entry`
- `planId`, `planName`, `billingInterval`, `priceUsd`
- `creditsGrantedUsd`, `deployLimit`, `introOfferApplied`
- `firstMonthEligible`, `isCurrentPlan`, `isRecommended`
- `autoRechargeSupported=true`
- validated optional OpenDesign campaign attribution already allowed by the
Vela registry
Go uses the legacy catalog values: zero credits, zero deploy limit, and not
recommended. Yearly credits are normalized to one month, matching the retired
modal.
Exposure is deferred until authenticated pricing context resolves. Deduplication
includes audience, interval, eligibility, current plan, and current interval,
so a state correction cannot be swallowed. Switching to Team clears the active
Personal exposure signature; returning to Personal creates a genuine
re-exposure. Repeating the same resolved state does not.
### Pricing click
Vela maps `pricing_click` inputs to the existing registry key
`subscription_pricing_click`, whose AMR PostHog event remains `ui_click`.
- Enabled Go/Plus/Pro/Max CTA: `subscribe_now` when no current Personal plan,
otherwise `upgrade_now`, with current and target plan/interval fields.
- User-initiated monthly/yearly change: `change_interval`, emitted before the
new interval's exposures.
- Enterprise lead open: `request_team_access`,
`area=enterprise_contact`, `targetDestination=lead_form`.
- Enterprise form submit intent: `team_lead_submit` before client validation or
network submission, preserving the legacy click meaning. Existing
`lead_submit_invalid`, `lead_submit_attempt`, `lead_submit_success`, and
`lead_submit_failed` events continue independently in OpenDesign PostHog.
Disabled/current/downgrade-unavailable Personal CTAs, Team checkout CTAs,
removed email/story/proof controls, and programmatic interval synchronization
do not emit compatibility clicks.
## Failure and Privacy Behavior
Compatibility analytics are best effort. A timeout, `401`, validation failure,
network error, or unavailable Vela endpoint must not block rendering, form
validation, checkout navigation, or lead submission. Failures may be logged in
development but must not expose request bodies or user data.
The endpoint is authenticated, origin-restricted, schema-restricted, and
rate-limited. It stores no additional cookies and accepts no PII. Direct public
traffic cannot write to the AMR funnel, preserving the original population.
## Rollout
Deploy the Vela endpoint first. The OpenDesign emitter may then be deployed
without a compatibility window or feature migration. Until Vela is available,
OpenDesign continues its existing local Pricing analytics and checkout behavior;
the compatibility post simply fails open.
The two PRs cross-link each other and state the deployment order. The existing
AMR dashboard and alert continue querying
`subscription_plan_exposure` and `ui_click` with
`registry_key=subscription_pricing_click`; no data-source migration is required.
## Testing and Acceptance
### Vela
- Route tests prove anonymous and untrusted-origin requests are rejected.
- Contract tests prove arbitrary registry keys, unknown plans/elements,
malformed/stale times, oversized batches, and free-form fields are rejected.
- Mapping tests prove both registry entries receive exact legacy event names,
types, common metadata, `pageName=workspace`, and the correct
`workspaceTab`.
- Repository tests prove the resulting PostHog payloads remain
`subscription_plan_exposure` and `ui_click` with the legacy `registry_key`.
### OpenDesign
- Pure contract tests cover Go/Plus/Pro/Max price and grant payloads, strict
source resolution, context-aware deduplication, click classification, and
excluded interactions.
- DOM/browser integration tests cover authenticated context resolution before
first exposure, same-interval state correction, audience leave/return,
interval control ordering, enabled/disabled CTAs, Enterprise submit intent,
and navigation-safe endpoint delivery.
- Existing `page_view`, `ui_click`, lead-form analytics, pricing UI, and checkout
destinations remain unchanged.
- Full repository gates, typechecks, unit suites, and relevant browser tests
pass in both repositories.
## Documentation
Update the current Feishu tracking rows for `subscription_plan_exposure` and
`subscription_pricing_click` to state that the migrated public Pricing page
emits them only through the authenticated Vela bridge and preserves
`pageName=workspace` plus `workspaceTab`. No new event name or dashboard source
is introduced.