diff --git a/eslint.config.js b/eslint.config.js index b26b4f5..60029c9 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -23,6 +23,7 @@ export default tseslint.config({ "@typescript-eslint/no-unused-vars": "error", "@typescript-eslint/consistent-type-imports": "error", "@eslint-react/no-children-only": "off", + "@eslint-react/no-context-provider": "off", "import/order": [ "error", { diff --git a/packages/event-tracker/package.json b/packages/event-tracker/package.json index aab3249..d35f8f6 100644 --- a/packages/event-tracker/package.json +++ b/packages/event-tracker/package.json @@ -52,8 +52,7 @@ ], "devDependencies": { "@types/react": "^19.1.10", - "react": "^19.1.1", - "zod": "^4.0.17" + "react": "^19.1.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" diff --git a/packages/event-tracker/src/__tests__/debounce.test.tsx b/packages/event-tracker/src/__tests__/debounce.test.tsx index 5494814..821a083 100644 --- a/packages/event-tracker/src/__tests__/debounce.test.tsx +++ b/packages/event-tracker/src/__tests__/debounce.test.tsx @@ -1,11 +1,10 @@ import { render } from "@testing-library/react"; import { describe, it, expect, vi } from "vitest"; -import { z } from "zod"; import { createTracker } from "../tracker"; import { debounce } from "../utils/debounce"; -import { sleep, anyFn } from "./utils"; +import { sleep, anyFn, createSchema, isString, isObject } from "./utils"; describe("debounce", () => { beforeEach(() => { @@ -316,8 +315,18 @@ describe("Debounce Integration Tests", () => { batch: { enable: false }, schema: { schemas: { - clickSchema: z.object({ - action: z.string(), + clickSchema: createSchema((value: unknown): { action: string } => { + if (!isObject(value)) { + throw new Error("Expected object"); + } + + const { action } = value; + + if (!isString(action)) { + throw new Error("action must be a string"); + } + + return { action }; }), }, }, diff --git a/packages/event-tracker/src/__tests__/schema.test.tsx b/packages/event-tracker/src/__tests__/schema.test.tsx index 46d88e2..6fbc0f1 100644 --- a/packages/event-tracker/src/__tests__/schema.test.tsx +++ b/packages/event-tracker/src/__tests__/schema.test.tsx @@ -1,10 +1,9 @@ import { render, renderHook } from "@testing-library/react"; import { vi } from "vitest"; -import { z } from "zod"; import { createTracker } from ".."; -import { sleep } from "./utils"; +import { createSchema, isNumber, isObject, isString, sleep } from "./utils"; const initFn = vi.fn(); const clickFn = vi.fn(); @@ -17,12 +16,28 @@ interface Context { interface Params {} const schemas = { - test_button_click: z - .object({ - text: z.string(), - button_id: z.number(), - }) - .strict(), + test_button_click: createSchema((value: unknown): { text: string; button_id: number } => { + if (!isObject(value)) { + throw new Error("Expected object"); + } + + const { text, button_id, ...rest } = value; + + // strict validation - no extra properties allowed + if (Object.keys(rest).length > 0) { + throw new Error(`Unexpected properties: ${Object.keys(rest).join(", ")}`); + } + + if (!isString(text)) { + throw new Error("text must be a string"); + } + + if (!isNumber(button_id)) { + throw new Error("button_id must be a number"); + } + + return { text, button_id }; + }), }; const [Track, useTracker] = createTracker({ @@ -63,7 +78,7 @@ describe("schemas", async () => { expect(clickFn).toHaveBeenCalledWith({ text: "click", button_id: 2, userId: "id" }); }); - it("throws error when schema is not defined in config", async () => { + it("validates schema and calls onSchemaError when validation fails", async () => { const page = render( {/* No 'button_id', so error expected */} @@ -77,6 +92,40 @@ describe("schemas", async () => { page.getByText("click").click(); await sleep(1); - expect(schemaErrorFn).toHaveBeenCalled(); + expect(schemaErrorFn).toHaveBeenCalledWith([{ message: "button_id must be a number" }]); + }); + + it("validates schema and rejects extra properties (strict mode)", async () => { + const page = render( + + {/* Extra property 'extra_field' should cause validation error */} + {/* @ts-expect-error */} + + + + , + ); + + page.getByText("click").click(); + await sleep(1); + + expect(schemaErrorFn).toHaveBeenCalledWith([{ message: "Unexpected properties: extra_field" }]); + }); + + it("validates schema and rejects wrong types", async () => { + const page = render( + + {/* Wrong type for button_id (string instead of number) */} + {/* @ts-expect-error */} + + + + , + ); + + page.getByText("click").click(); + await sleep(1); + + expect(schemaErrorFn).toHaveBeenCalledWith([{ message: "button_id must be a number" }]); }); }); diff --git a/packages/event-tracker/src/__tests__/throttle.test.tsx b/packages/event-tracker/src/__tests__/throttle.test.tsx index f90a00d..a87f9f5 100644 --- a/packages/event-tracker/src/__tests__/throttle.test.tsx +++ b/packages/event-tracker/src/__tests__/throttle.test.tsx @@ -1,11 +1,10 @@ import { render } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; -import { z } from "zod"; import { createTracker } from "../tracker"; import { throttle } from "../utils/throttle"; -import { sleep, anyFn } from "./utils"; +import { sleep, anyFn, createSchema, isString, isObject } from "./utils"; describe("throttle", () => { beforeEach(() => { @@ -409,8 +408,18 @@ describe("Throttle Integration Tests", () => { batch: { enable: false }, schema: { schemas: { - clickSchema: z.object({ - action: z.string(), + clickSchema: createSchema((value: unknown): { action: string } => { + if (!isObject(value)) { + throw new Error("Expected object"); + } + + const { action } = value; + + if (!isString(action)) { + throw new Error("action must be a string"); + } + + return { action }; }), }, }, diff --git a/packages/event-tracker/src/__tests__/utils.ts b/packages/event-tracker/src/__tests__/utils.ts index fbb95b5..85967fa 100644 --- a/packages/event-tracker/src/__tests__/utils.ts +++ b/packages/event-tracker/src/__tests__/utils.ts @@ -1,2 +1,30 @@ +import { StandardSchemaV1 } from ".."; + export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); export const anyFn = expect.any(Function); + +// Standard Schema compliant schema definition +export function createSchema(validator: (value: unknown) => T): StandardSchemaV1 { + return { + "~standard": { + version: 1, + vendor: "custom", + validate: (value: unknown) => { + try { + const result = validator(value); + return { value: result }; + } catch (error) { + return { + issues: [{ message: error instanceof Error ? error.message : "Validation failed" }], + }; + } + }, + }, + }; +} + +// Type guard functions +export const isString = (value: unknown): value is string => typeof value === "string"; +export const isNumber = (value: unknown): value is number => typeof value === "number"; +export const isObject = (value: unknown): value is Record => + value !== null && typeof value === "object" && !Array.isArray(value); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 269a340..57c6818 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -153,9 +153,6 @@ importers: react: specifier: ^19.1.1 version: 19.1.1 - zod: - specifier: ^4.0.17 - version: 4.0.17 packages: @@ -4977,9 +4974,6 @@ packages: zod@4.0.0-beta.20250424T163858: resolution: {integrity: sha512-fKhW+lEJnfUGo0fvQjmam39zUytARR2UdCEh7/OXJSBbKScIhD343K74nW+UUHu/r6dkzN6Uc/GqwogFjzpCXg==} - zod@4.0.17: - resolution: {integrity: sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ==} - zustand@5.0.7: resolution: {integrity: sha512-Ot6uqHDW/O2VdYsKLLU8GQu8sCOM1LcoE8RwvLv9uuRT9s6SOHCKs0ZEOhxg+I1Ld+A1Q5lwx+UlKXXUoCZITg==} engines: {node: '>=12.20.0'} @@ -10815,8 +10809,6 @@ snapshots: dependencies: '@zod/core': 0.9.0 - zod@4.0.17: {} - zustand@5.0.7(@types/react@19.1.9)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)): optionalDependencies: '@types/react': 19.1.9