redesign the homepage
Build & Deploy / Build & Push Docker Image (push) Failing after 47s
Build & Deploy / Deploy to VPS (push) Has been skipped
Test / API Unit Tests (push) Failing after 5m4s
Test / Marketplace Unit Tests (push) Failing after 4m55s
Test / Admin Unit Tests (push) Successful in 9m37s
Test / Dashboard Unit Tests (push) Successful in 9m37s
Test / API Integration Tests (push) Successful in 9m54s

This commit is contained in:
root
2026-06-26 16:27:21 -04:00
parent 256ff0814e
commit d7fb7b7a7b
1030 changed files with 107374 additions and 2657 deletions
+25
View File
@@ -0,0 +1,25 @@
'use client';
import { validateAnalyticsEvent, type AnalyticsContext, type AnalyticsEventName } from './events';
declare global {
interface Window {
__rdgAnalyticsTestSink?: Array<{ name: AnalyticsEventName; context: AnalyticsContext }>;
}
}
function analyticsMode(): 'disabled' | 'test' {
return process.env.NEXT_PUBLIC_ANALYTICS_MODE === 'test' ? 'test' : 'disabled';
}
function consentGranted(): boolean {
return document.documentElement.dataset.analyticsConsent === 'granted';
}
export function trackAnalytics(name: AnalyticsEventName, context: AnalyticsContext): boolean {
const event = validateAnalyticsEvent(name, context);
if (!event || analyticsMode() !== 'test' || !consentGranted()) return false;
window.__rdgAnalyticsTestSink ??= [];
window.__rdgAnalyticsTestSink.push(event);
return true;
}
+88
View File
@@ -0,0 +1,88 @@
import { z } from 'zod';
export const analyticsEventNames = [
'page_view',
'section_view',
'navigation_click',
'locale_change',
'theme_change',
'mobile_nav_open',
'mobile_nav_close',
'product_tour_open',
'product_tour_step',
'product_tour_complete',
'product_tour_close',
'demo_open',
'demo_submit_attempt',
'demo_validation_error',
'demo_submit_success',
'demo_submit_failure',
'demo_close',
'faq_open',
'pricing_fleet_band_selected',
'pricing_billing_period_changed',
'pricing_plan_cta_clicked',
] as const;
export type AnalyticsEventName = (typeof analyticsEventNames)[number];
const safeText = z
.string()
.min(1)
.max(80)
.regex(/^[a-zA-Z0-9_-]+$/);
export const analyticsContextSchema = z
.object({
routeId: safeText,
locale: z.enum(['en', 'fr', 'ar']),
direction: z.enum(['ltr', 'rtl']),
resolvedTheme: z.enum(['light', 'dark']),
source: safeText.optional(),
step: z.number().int().min(1).max(100).optional(),
errorCode: safeText.optional(),
errorCount: z.number().int().min(1).max(20).optional(),
reason: safeText.optional(),
fleetBand: z.enum(['small', 'growing', 'scale', 'enterprise']).optional(),
billingPeriod: z.enum(['monthly', 'annual']).optional(),
plan: z.enum(['launch', 'growth', 'enterprise']).optional(),
})
.strict();
export type AnalyticsContext = z.output<typeof analyticsContextSchema>;
export interface AnalyticsEventDefinition {
name: AnalyticsEventName;
purpose: string;
consentCategory: 'analytics';
personalData: false;
}
export const analyticsEventRegistry: readonly AnalyticsEventDefinition[] = analyticsEventNames.map(
(name) => ({
name,
purpose: `Phase 9 approved ${name} interaction measurement.`,
consentCategory: 'analytics',
personalData: false,
}),
);
const forbiddenPayloadKeys = new Set([
'name',
'email',
'company',
'message',
'phone',
'fieldvalue',
'serverresponse',
]);
export function validateAnalyticsEvent(name: unknown, context: unknown) {
const event = z.enum(analyticsEventNames).safeParse(name);
const payload = analyticsContextSchema.safeParse(context);
if (!event.success || !payload.success) return null;
if (Object.keys(payload.data).some((key) => forbiddenPayloadKeys.has(key.toLowerCase()))) {
return null;
}
return { name: event.data, context: payload.data } as const;
}