import { z } from 'zod/v4'; import type { TelemetryEventInput } from './define'; type ValidationPath = Array; type ObjectSchema = z.ZodObject>; type UnionSchema = z.ZodUnion; type ArraySchema = z.ZodArray; type RecordSchema = z.ZodRecord; type ReadonlySchema = z.ZodReadonly; type WrappedSchema = | z.ZodOptional | z.ZodNullable | z.ZodDefault | z.ZodNonOptional | z.ZodCatch | z.ZodLazy; function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } function formatPath(path: ValidationPath): string { return path.map(String).join('.') || '(root)'; } function isObjectSchema(schema: z.ZodType): schema is ObjectSchema { return schema instanceof z.ZodObject; } function isUnionSchema(schema: z.ZodType): schema is UnionSchema { return schema instanceof z.ZodUnion; } function isArraySchema(schema: z.ZodType): schema is ArraySchema { return schema instanceof z.ZodArray; } function isRecordSchema(schema: z.ZodType): schema is RecordSchema { return schema instanceof z.ZodRecord; } function isWrappedSchema(schema: z.ZodType): schema is WrappedSchema { return ( schema instanceof z.ZodOptional || schema instanceof z.ZodNullable || schema instanceof z.ZodDefault || schema instanceof z.ZodNonOptional || schema instanceof z.ZodCatch || schema instanceof z.ZodLazy ); } function isReadonlySchema(schema: z.ZodType): schema is ReadonlySchema { return schema instanceof z.ZodReadonly; } function isSchema(value: unknown): value is z.ZodType { return value instanceof z.ZodType; } function getWrappedSchema(schema: z.ZodType): z.ZodType | undefined { if (isWrappedSchema(schema)) { return schema.unwrap(); } if (isReadonlySchema(schema)) { return schema.def.innerType; } return undefined; } function getUnrecognizedPropertyIssues( schema: z.ZodType, value: unknown, path: ValidationPath = [], ): string[] { const wrappedSchema = getWrappedSchema(schema); if (wrappedSchema !== undefined) { return getUnrecognizedPropertyIssues(wrappedSchema, value, path); } if (isObjectSchema(schema)) { if (!isRecord(value)) return []; const issues: string[] = []; for (const [key, childValue] of Object.entries(value)) { const childPath = [...path, key]; const childSchema = schema.shape[key]; if (childSchema !== undefined) { issues.push(...getUnrecognizedPropertyIssues(childSchema, childValue, childPath)); } else if (schema.def.catchall === undefined) { issues.push(`${formatPath(childPath)}: unrecognized property`); } else if (isSchema(schema.def.catchall)) { issues.push(...getUnrecognizedPropertyIssues(schema.def.catchall, childValue, childPath)); } } return issues; } if (isArraySchema(schema)) { if (!Array.isArray(value)) return []; return value.flatMap((item, index) => getUnrecognizedPropertyIssues(schema.element, item, [...path, index]), ); } if (isRecordSchema(schema)) { if (!isRecord(value)) return []; return Object.entries(value).flatMap(([key, childValue]) => getUnrecognizedPropertyIssues(schema.valueType, childValue, [...path, key]), ); } if (isUnionSchema(schema)) { const matchingOptions = schema.options.filter((option) => option.safeParse(value).success); if (matchingOptions.length === 0) return []; return matchingOptions .map((option) => getUnrecognizedPropertyIssues(option, value, path)) .reduce((fewestIssues, optionIssues) => optionIssues.length < fewestIssues.length ? optionIssues : fewestIssues, ); } return []; } export function getEventValidationError( event: TelemetryEventInput, properties: unknown, ): string | null { const issues: string[] = []; const result = event.properties.safeParse(properties); if (!result.success) { for (const issue of result.error.issues) { issues.push(`${issue.path.map(String).join('.') || '(root)'}: ${issue.message}`); } } issues.push(...getUnrecognizedPropertyIssues(event.properties, properties)); if (issues.length === 0) return null; return `Telemetry event "${event.name}" failed schema validation: ${issues.join('; ')}`; }