Skip to content

TypeBox => Zod - #812

Open
acburdine wants to merge 2 commits into
mainfrom
feat/typebox-zod
Open

TypeBox => Zod#812
acburdine wants to merge 2 commits into
mainfrom
feat/typebox-zod

Conversation

@acburdine

Copy link
Copy Markdown
Member

This PR converts the existing validation logic via TypeBox to Zod-based schemas. Normally, Zod schemas would negatively impact performance (ajv + TypeBox both use json-schema based compilation/validation under the hood), but with this approach we get comparable performance by converting the Zod schemas to JSON schemas at runtime, then compiling them with ajv to get maximum throughput.

Additionally, this PR makes a slight tweak to event_id processing. Largely the behavior is the same, but removing the need for transforming the event_id payload during validation (instead using a relaxed any value for validation and then a manual check after the fact.

ref https://linear.app/ghost/issue/PLA-280/optimize-traffic-analytics-docker-buildself-hosting-execution
- replace Type.Transform with Type.Any(), add resolveEventId function to standardize event id resolution
- fix handling of non-canonical timestamps
- update tests
ref https://linear.app/ghost/issue/PLA-280/optimize-traffic-analytics-docker-buildself-hosting-execution
- replace typebox with zod => ajv json schema pipeline
- zod + ajv has relatively similar performance characteristics to TypeBox (esp. when the event_id fix is included)
- add separate parser for pubsub
@acburdine
acburdine requested a review from cmraible as a code owner July 30, 2026 23:15
@acburdine
acburdine requested a review from JoeeGrigg July 30, 2026 23:15
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Migrated page-hit schemas from TypeBox to Zod with inferred types and explicit event-ID resolution. Added Zod-to-AJV compilation, separate AJV validator configurations, Fastify validator and serializer compilers, and Zod type-provider mappings. Updated routes and batch processing to use the new validation path, and expanded unit and integration coverage for schema parity, timestamps, UUIDs, and validation errors.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested reviewers: cmraible, joeegrigg

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the primary migration from TypeBox to Zod.
Description check ✅ Passed The description accurately summarizes the TypeBox-to-Zod migration, Ajv compilation approach, and event_id processing change.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/typebox-zod

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/schemas/v1/page-hit-processed.ts (1)

146-190: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Settle event_id before building PageHitProcessed.

The raw payload accepts an empty event_id string, but transformPageHitRawToProcessed only substitutes a generated UUID for null/undefined. An empty-string value would reach the processed payload as invalid z.guid() input, while the request-to-publication path already settles it with resolveEventId. Use resolveEventId(pageHitRaw.payload.event_id) here too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/schemas/v1/page-hit-processed.ts` around lines 146 - 190, The
transformPageHitRawToProcessed function must resolve empty event IDs before
constructing the processed payload. Replace the current nullish-only event_id
fallback with resolveEventId(pageHitRaw.payload.event_id), preserving generated
UUID behavior for missing or empty values and ensuring the processed payload
receives a valid ID.
🧹 Nitpick comments (3)
test/unit/schemas/v1/page-hit-request.test.ts (1)

11-11: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

New node:assert/strict import mixes assertion styles with the rest of the suite.

The rest of the file (and the sibling schema test files) uses vitest's expect(...).toBe(...). assert.ok(...) here is functionally fine but introduces a second assertion library for no apparent reason.

✏️ Suggested consistency fix
-import assert from 'node:assert/strict';
+
-            it(`should validate with ${label} as the event ID`, () => {
-                assert.ok(EventIdSchema.safeParse(value).success, `Event ID can be ${label}`);
-            });
+            it(`should validate with ${label} as the event ID`, () => {
+                expect(EventIdSchema.safeParse(value).success).toBe(true);
+            });

Also applies to: 27-31

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/unit/schemas/v1/page-hit-request.test.ts` at line 11, Remove the
node:assert/strict import from the page-hit request test and replace its
assert.ok usage with Vitest’s expect(...).toBe(...) style, matching the existing
assertions in the file and sibling schema tests.

Source: Coding guidelines

src/schemas/v1/page-hit-request.ts (1)

6-13: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Common primitive schemas (StringSchema, NonEmptyStringSchema, UUIDSchema, ISO8601DateTimeSchema) are copy-pasted verbatim across the three v1 schema files, and have already drifted once (request.ts's NonEmptyStringSchema adds a whitespace-rejecting regex the other two lack). A shared module would prevent future divergence (e.g. if the ISO precision or guid pattern needs to change).

  • src/schemas/v1/page-hit-request.ts#L6-L13: extract to a shared module; keep the extra .regex() constraint local if it's intentionally request-only.
  • src/schemas/v1/page-hit-raw.ts#L4-L8: import the shared primitives instead of redefining them.
  • src/schemas/v1/page-hit-processed.ts#L10-L13: import the shared primitives instead of redefining them.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/schemas/v1/page-hit-request.ts` around lines 6 - 13, Extract the shared
StringSchema, UUIDSchema, and ISO8601DateTimeSchema primitives from
src/schemas/v1/page-hit-request.ts, along with the base NonEmptyStringSchema,
into a shared module; keep page-hit-request.ts’s whitespace-rejecting regex as a
local request-specific constraint. Update src/schemas/v1/page-hit-raw.ts and
src/schemas/v1/page-hit-processed.ts to import the shared primitives instead of
redefining them, preserving their existing schema behavior.
test/unit/schemas/validation.test.ts (1)

71-98: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No AJV-parity case for EventIdSchema = z.any().

z.any() looks like a new schema construct for this codebase (accept-anything, resolved separately by resolveEventId). Consider adding a case (e.g. payload.event_id: 123 or {}) to the cases array to confirm ajv's projection of z.any() doesn't reject non-string values, matching Zod's behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/unit/schemas/validation.test.ts` around lines 71 - 98, Add an AJV-parity
case to the validation cases array for PageHitRequestBodySchema where
payload.event_id is a non-string value such as a number or object. Keep the case
valid and verify that AJV accepts it consistently with EventIdSchema’s z.any()
behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/schemas/v1/page-hit-processed.ts`:
- Around line 146-190: The transformPageHitRawToProcessed function must resolve
empty event IDs before constructing the processed payload. Replace the current
nullish-only event_id fallback with resolveEventId(pageHitRaw.payload.event_id),
preserving generated UUID behavior for missing or empty values and ensuring the
processed payload receives a valid ID.

---

Nitpick comments:
In `@src/schemas/v1/page-hit-request.ts`:
- Around line 6-13: Extract the shared StringSchema, UUIDSchema, and
ISO8601DateTimeSchema primitives from src/schemas/v1/page-hit-request.ts, along
with the base NonEmptyStringSchema, into a shared module; keep
page-hit-request.ts’s whitespace-rejecting regex as a local request-specific
constraint. Update src/schemas/v1/page-hit-raw.ts and
src/schemas/v1/page-hit-processed.ts to import the shared primitives instead of
redefining them, preserving their existing schema behavior.

In `@test/unit/schemas/v1/page-hit-request.test.ts`:
- Line 11: Remove the node:assert/strict import from the page-hit request test
and replace its assert.ok usage with Vitest’s expect(...).toBe(...) style,
matching the existing assertions in the file and sibling schema tests.

In `@test/unit/schemas/validation.test.ts`:
- Around line 71-98: Add an AJV-parity case to the validation cases array for
PageHitRequestBodySchema where payload.event_id is a non-string value such as a
number or object. Keep the case valid and verify that AJV accepts it
consistently with EventIdSchema’s z.any() behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: db684646-4d0a-4aa5-a178-32fed5fdee66

📥 Commits

Reviewing files that changed from the base of the PR and between 40cdf91 and 49582de.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (19)
  • AGENTS.md
  • package.json
  • src/app.ts
  • src/handlers/page-hit-handlers.ts
  • src/schemas/format-registry.ts
  • src/schemas/index.ts
  • src/schemas/v1/page-hit-processed.ts
  • src/schemas/v1/page-hit-raw.ts
  • src/schemas/v1/page-hit-request.ts
  • src/schemas/validation.ts
  • src/services/batch-worker/BatchWorker.ts
  • src/transformations/page-hit-transformations.ts
  • src/types/tryghost-validator.d.ts
  • test/integration/app.test.ts
  • test/integration/validation-error-logging.test.ts
  • test/unit/schemas/v1/page-hit-processed.test.ts
  • test/unit/schemas/v1/page-hit-raw.test.ts
  • test/unit/schemas/v1/page-hit-request.test.ts
  • test/unit/schemas/validation.test.ts
💤 Files with no reviewable changes (2)
  • src/types/tryghost-validator.d.ts
  • src/schemas/format-registry.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant