Skip to content

feat: add public event validator [agent:clawd]#10

Open
TargiX wants to merge 3 commits into
mainfrom
clawd/signalops-event-validator
Open

feat: add public event validator [agent:clawd]#10
TargiX wants to merge 3 commits into
mainfrom
clawd/signalops-event-validator

Conversation

@TargiX

@TargiX TargiX commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Why now

SignalOps already has a strong guided cockpit replay, but prospective integrations had no safe way to test the event contract. This recovers one bounded, useful slice from the broader uncommitted productization experiment without bringing in auth, storage, SDK, or pilot-workflow scope.

What changed

  • adds a public zero-storage POST /api/events/validate endpoint
  • validates single events and batches up to 100 events / 256 KB
  • supports partial batch rejection with per-item diagnostics
  • redacts user and prompt in accepted previews
  • enforces strict event types, UTC timestamps, bounded identifiers, and finite nonnegative telemetry
  • adds a polished /validate playground with editable samples, readiness coverage, gaps, and next actions
  • adds focused Node contract/request tests

Verification

  • node --experimental-strip-types --disable-warning=MODULE_TYPELESS_PACKAGE_JSON scripts/test-signalops-events.mjs
  • ./node_modules/.bin/eslint .
  • ./node_modules/.bin/tsc --noEmit --pretty false
  • ./node_modules/.bin/next build
  • Codex review: two passes; lifecycle ID, timestamp strictness, readiness semantics, a11y, retry ID uniqueness, and retry-coverage findings fixed
  • Hermes autoreview was attempted but blocked before review because the installed Codex CLI is too old for its configured gpt-5.6-sol model; manual Codex review used gpt-5.4 instead

agent:clawd

Ready for Ilya's manual review and merge. No auto-merge enabled.

Summary by CodeRabbit

  • New Features

    • Added a public validator page to submit JSON event/batch payloads and view validation outcomes (accepted/rejected counts, diagnostics, coverage, gaps, and next steps).
    • Introduced end-to-end event normalization and request validation with batch/item limits, UTC timestamp checks, and required-field enforcement.
    • Implemented privacy redaction for sensitive user/prompt preview data.
    • Added an API endpoint that returns consistent success/error responses, including size/content-type failures and internal error handling.
  • Tests

    • Added comprehensive automated tests covering normalization stability, validation diagnostics, redaction, and request edge cases (invalid JSON, unsupported types, and payload-size limits).

@vercel

vercel Bot commented Jul 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
signalops Ready Ready Preview, Comment Jul 12, 2026 11:25am

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 56 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: db022524-3155-4ec1-8093-f4df6f6a2e17

📥 Commits

Reviewing files that changed from the base of the PR and between bd3e4f0 and 539c835.

📒 Files selected for processing (1)
  • scripts/test-signalops-events.mjs
📝 Walkthrough

Walkthrough

Adds SignalOps event normalization and validation, an API endpoint, a browser-based public validator, and deterministic checks for event identity, redaction, diagnostics, request limits, and validation responses.

Changes

SignalOps validation

Layer / File(s) Summary
Event contract and normalization
src/lib/signalops/events.ts
Defines event types, limits, normalized event shapes, strict field validation, deterministic identifiers, privacy redaction, batch rejection, and readiness diagnostics.
HTTP request validation and API wiring
src/lib/signalops/events.ts, src/app/api/events/validate/route.ts
Validates content type, body size, JSON, and event batches, then exposes the resulting status and response through a Node.js POST route.
Public validator page
src/app/validate/page.tsx
Adds sample payloads, JSON editing, local parsing, validation requests, clipboard support, and conditional rendering of validation results and diagnostics.
Validation regression checks
scripts/test-signalops-events.mjs
Checks deterministic normalization, redaction, retry identity, diagnostics, HTTP errors, size limits, batch limits, successful responses, and rejected-event responses.

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

Sequence Diagram(s)

sequenceDiagram
  participant ValidatorPage
  participant ValidateRoute
  participant EventValidator
  ValidatorPage->>ValidateRoute: POST event payload
  ValidateRoute->>EventValidator: validateSignalEventRequest(request)
  EventValidator-->>ValidateRoute: status and validation response
  ValidateRoute-->>ValidatorPage: JSON validation result
  ValidatorPage-->>ValidatorPage: render counts, diagnostics, and previews
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding a public event validator.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch clawd/signalops-event-validator

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

@TargiX

TargiX commented Jul 12, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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.

Actionable comments posted: 6

🧹 Nitpick comments (5)
scripts/test-signalops-events.mjs (2)

160-167: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

This test doesn't isolate the early Content-Length short-circuit.

tooLargePayload plus its wrapping quotes already exceeds maxBodyBytes, so the fallback UTF-8 byte-length check (independent of the content-length header) would also yield 413 here. This test can't distinguish whether requestContentLengthExceeds itself is actually triggering the rejection. Consider a dedicated case with a small real body and an inflated content-length header to verify the early-rejection path specifically.

🤖 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 `@scripts/test-signalops-events.mjs` around lines 160 - 167, The test around
validateSignalEventRequest does not isolate the requestContentLengthExceeds
early-rejection path. Replace or supplement the oversized-body case with a small
valid JSON body whose inflated content-length header exceeds
SIGNALOPS_EVENT_LIMITS.maxBodyBytes, and assert status 413; keep the actual body
below the limit so fallback UTF-8 validation cannot cause the rejection.

30-263: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the cost.recorded validation branch.

The suite exercises generation.started/completed/retrying and provider.health, but never cost.recorded, even though normalizeSignalEvent has a dedicated required-field check for it (providerId + cost). Consider adding a positive case and a negative case (missing cost/providerId) to lock in this 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 `@scripts/test-signalops-events.mjs` around lines 30 - 263, Add positive and
negative validation coverage near the existing event normalization and batch
assertions for the `cost.recorded` type. Use `normalizeSignalEvent` or
`normalizeSignalEventBatch` to verify a valid event containing both required
`providerId` and `cost` fields succeeds, and verify cases missing either field
are rejected with the expected validation error.
src/app/validate/page.tsx (3)

143-166: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Overly complex type derivation for readinessTone's parameter.

This nested extends infer T ? T extends {...} ? R : never : never idiom is used to force distribution over the optional diagnostics union, but it's needlessly hard to read for what NonNullable<...>["readiness"] already achieves directly.

♻️ Proposed simplification
-function readinessTone(readiness: ValidationResponse["diagnostics"] extends infer T
-  ? T extends { readiness: infer R }
-    ? R
-    : never
-  : never) {
+function readinessTone(readiness: NonNullable<ValidationResponse["diagnostics"]>["readiness"]) {
🤖 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/app/validate/page.tsx` around lines 143 - 166, Update the readinessTone
parameter type to use
NonNullable<ValidationResponse["diagnostics"]>["readiness"] instead of the
nested conditional infer expression. Preserve the existing readinessTone
behavior and return values unchanged.

364-368: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

aria-live="polite" scope is too broad.

The live region wraps the entire result card — icon, heading, accepted/rejected counts, coverage chips, gaps, next actions, rejected list, and accepted preview (through line 519). Every validation run causes screen readers to re-announce this whole block, which is excessive; scope aria-live to just the status heading/summary text instead.

♻️ Proposed refactor
-          <div
-            aria-live="polite"
-            className="rounded-[28px] border border-white/75 bg-white/90 p-5 shadow-[var(--shadow-2)] backdrop-blur sm:p-6"
-          >
-            <div className="flex items-center gap-3">
+          <div className="rounded-[28px] border border-white/75 bg-white/90 p-5 shadow-[var(--shadow-2)] backdrop-blur sm:p-6">
+            <div aria-live="polite" className="flex items-center gap-3">
🤖 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/app/validate/page.tsx` around lines 364 - 368, Move aria-live="polite"
from the outer result-card container to the status heading/summary element
within the validation result markup, keeping the live region limited to the
validation status text and counts. Leave the icon, coverage details, actions,
rejected list, and accepted preview outside the live region.

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

Duplicate hand-rolled response type instead of the shared contract type.

ValidationResponse re-declares the shape already defined server-side as SignalEventValidationSummary/SignalEventValidationResponse in src/lib/signalops/events.ts. Since this is a type-only need, it can be imported with import type (erased at compile time, safe for a "use client" file) instead of hand-copied, removing drift risk if the server contract changes.

♻️ Proposed refactor
-type ValidationResponse = {
-  ok: boolean;
-  code?: string;
-  error?: string;
-  partial?: boolean;
-  verificationOnly?: boolean;
-  storedEvents?: number;
-  validEvents?: number;
-  rejectedEvents?: number;
-  eventTypes?: string[];
-  providerIds?: string[];
-  modelIds?: string[];
-  requestId?: string;
-  limits?: {
-    maxBodyBytes: number;
-    maxBatchEvents: number;
-  };
-  diagnostics?: {
-    readiness: "insufficient" | "partial" | "pilot_ready";
-    coverage: {
-      generationLifecycle: {
-        started: boolean;
-        completed: boolean;
-        failed: boolean;
-        retrying: boolean;
-      };
-      providerHealth: boolean;
-      latency: boolean;
-      cost: boolean;
-      retries: boolean;
-      providers: number;
-      models: number;
-    };
-    gaps: string[];
-    nextActions: string[];
-  };
-  rejected?: Array<{ index: number; error: string }>;
-  acceptedPreview?: Array<Record<string, unknown>>;
-};
+import type { SignalEventValidationResponse } from "`@/lib/signalops/events`";
+
+// Keep fields optional here since we're parsing an untrusted network response.
+type ValidationResponse = Partial<SignalEventValidationResponse>;
🤖 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/app/validate/page.tsx` around lines 6 - 44, Replace the hand-rolled
ValidationResponse type with the shared SignalEventValidationSummary or
SignalEventValidationResponse contract from the signal events module. Add it as
a type-only import so the "use client" file has no runtime dependency, and
update local references to use the imported contract while removing the
duplicate declaration.
🤖 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.

Inline comments:
In `@src/app/api/events/validate/route.ts`:
- Around line 9-12: Wrap the POST handler’s validateSignalEventRequest call in a
defensive try/catch so unexpected failures still return the documented JSON
error shape, including ok, code, error, and requestId fields, with an
appropriate server-error status. Preserve the existing response.body and
response.status behavior for successful validation and handled errors.
- Line 1: Update the NextResponse import in the route to use the exported
`next/server` subpath instead of `next/server.js`, preserving the existing
NextResponse usage.

In `@src/app/validate/page.tsx`:
- Line 338: Update the payload textarea’s onChange handler to clear the existing
copied, error, and result state before or alongside setPayload, matching the
reset behavior in loadSample. Ensure editing the JSON immediately removes stale
validation feedback and previous results.
- Around line 203-221: Update the validation request flow around the fetch call
to use an AbortController with a finite timeout, pass its signal to fetch, and
abort the request when the timeout expires. Preserve the existing setError
handling and ensure setIsValidating(false) still runs through the existing
finally block after timeout or other request failures.
- Line 340: Replace the textarea’s outline-none utility in the className at the
validation page textarea with outline-hidden, preserving all other styling
classes unchanged.

In `@src/lib/signalops/events.ts`:
- Around line 509-535: The validateSignalEventRequest function currently buffers
the entire request via request.text() before enforcing the body limit. Replace
that read with incremental streaming from request.body, tracking UTF-8 byte
length and stopping as soon as SIGNALOPS_EVENT_LIMITS.maxBodyBytes is exceeded,
while preserving the existing 413 error response and downstream body parsing for
valid payloads.

---

Nitpick comments:
In `@scripts/test-signalops-events.mjs`:
- Around line 160-167: The test around validateSignalEventRequest does not
isolate the requestContentLengthExceeds early-rejection path. Replace or
supplement the oversized-body case with a small valid JSON body whose inflated
content-length header exceeds SIGNALOPS_EVENT_LIMITS.maxBodyBytes, and assert
status 413; keep the actual body below the limit so fallback UTF-8 validation
cannot cause the rejection.
- Around line 30-263: Add positive and negative validation coverage near the
existing event normalization and batch assertions for the `cost.recorded` type.
Use `normalizeSignalEvent` or `normalizeSignalEventBatch` to verify a valid
event containing both required `providerId` and `cost` fields succeeds, and
verify cases missing either field are rejected with the expected validation
error.

In `@src/app/validate/page.tsx`:
- Around line 143-166: Update the readinessTone parameter type to use
NonNullable<ValidationResponse["diagnostics"]>["readiness"] instead of the
nested conditional infer expression. Preserve the existing readinessTone
behavior and return values unchanged.
- Around line 364-368: Move aria-live="polite" from the outer result-card
container to the status heading/summary element within the validation result
markup, keeping the live region limited to the validation status text and
counts. Leave the icon, coverage details, actions, rejected list, and accepted
preview outside the live region.
- Around line 6-44: Replace the hand-rolled ValidationResponse type with the
shared SignalEventValidationSummary or SignalEventValidationResponse contract
from the signal events module. Add it as a type-only import so the "use client"
file has no runtime dependency, and update local references to use the imported
contract while removing the duplicate declaration.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9ac9d0d5-9549-4738-aaeb-35cbde7af412

📥 Commits

Reviewing files that changed from the base of the PR and between 6e49173 and abb129f.

📒 Files selected for processing (4)
  • scripts/test-signalops-events.mjs
  • src/app/api/events/validate/route.ts
  • src/app/validate/page.tsx
  • src/lib/signalops/events.ts

Comment thread src/app/api/events/validate/route.ts Outdated
Comment thread src/app/api/events/validate/route.ts
Comment thread src/app/validate/page.tsx
Comment thread src/app/validate/page.tsx Outdated
Comment thread src/app/validate/page.tsx Outdated
Comment thread src/lib/signalops/events.ts

@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)
scripts/test-signalops-events.mjs (1)

288-304: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the negative-duration test isolate the telemetry rule.

The test only asserts that the event is rejected, so it can remain green if rejection is caused by another field instead of durationMs: -1. Use a known-valid timestamp and assert the rejection diagnostic specifically mentions durationMs.

🐛 Proposed fix
         {
           type: "generation.completed",
-          occurredAt: "3026-07-12T12:00:00.000Z",
+          occurredAt: isoOffset(-60_000),
           generationId: "gen_invalid_001",
           providerId: "fal",
           modelId: "flux-kontext-pro",
           durationMs: -1,
         },

Also assert the rejected item's existing diagnostic field contains durationMs.

🤖 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 `@scripts/test-signalops-events.mjs` around lines 288 - 304, Update the
negative-duration test event in the noValidResponse setup to use a known-valid
timestamp, ensuring rejection is isolated to durationMs: -1. Extend the
assertions for noValidJson.rejectedEvents to verify the rejected item’s existing
diagnostic field specifically mentions durationMs, while preserving the current
response-count assertions.
🤖 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 `@scripts/test-signalops-events.mjs`:
- Around line 288-304: Update the negative-duration test event in the
noValidResponse setup to use a known-valid timestamp, ensuring rejection is
isolated to durationMs: -1. Extend the assertions for noValidJson.rejectedEvents
to verify the rejected item’s existing diagnostic field specifically mentions
durationMs, while preserving the current response-count assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9f1b129f-0595-4017-9dbf-3d2936229b16

📥 Commits

Reviewing files that changed from the base of the PR and between abb129f and bd3e4f0.

📒 Files selected for processing (4)
  • scripts/test-signalops-events.mjs
  • src/app/api/events/validate/route.ts
  • src/app/validate/page.tsx
  • src/lib/signalops/events.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/app/validate/page.tsx
  • src/lib/signalops/events.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