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
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:
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user