npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@uptimizr/schema

v1.2.0

Published

Uptimizr event contracts — the single source of truth for all analytics events (Zod schemas + TypeScript types).

Downloads

1,386

Readme

@uptimizr/schema

The single source of truth for every Uptimizr analytics event. Client SDKs, the collector server, and the replay package all import event shapes from here — they are never redefined elsewhere.

Built with Zod: each event has a runtime schema and an inferred TypeScript type. Events are replay-complete (ordered, timestamped, keyed by sessionId) and the design is registry-based so new events and fields can be added without breaking existing producers or consumers.

Install

pnpm add @uptimizr/schema

Usage

import { anyEventSchema, collectRequestSchema, type CameraSampleEvent } from "@uptimizr/schema";

// Validate a single event of unknown type (discriminated union on `type`).
const result = anyEventSchema.safeParse(incoming);

// Validate a batch posted to /api/v1/collect.
const batch = collectRequestSchema.parse(requestBody);

Event envelope

Every event carries a shared envelope:

| Field | Notes | | ----------------- | ----------------------------------------------------------- | | projectId | Public project identifier. | | visitorId | Server-set daily-rotating hash. Clients omit it. | | sessionId | Groups events from one visit (client-generated, in-memory). | | ts | Epoch milliseconds. | | sdkVersion | Producing SDK version. | | sceneId | Optional developer-assigned scene/area id. | | url, pageMeta | Optional page context. |

Event catalog (v1)

| type | Purpose | | --------------------- | ------------------------------------------------------------------------------------------------------------------- | | session_start | Session begins; carries device, graphics, scene, connector, and opt-in user metadata. | | session_end | Session ends; duration + reason. | | frame_perf | Sampled FPS / frame-time window, with optional percentile/jank/render-scale/spatial fields. | | camera_sample | Camera position, direction, target, fov, and optional gaze hit — view-direction heatmap. | | node_transform | Replay-complete transforms for developer-declared scene actors / bones / subtree children. | | pointer_move | Screen-normalized position + optional 3D hit + input-source metadata. | | pointer_click | Click heatmap event with screen/hit/button/input-source metadata. | | pointer_down | Pointer/button press transition. | | pointer_up | Pointer/button release transition. | | camera_gesture | Typed navigation gesture (orbit/pan/dolly/zoom/roll/fly/navigate). | | mesh_interaction | Hover / pick / click / drag / teleport on a named mesh. | | mesh_visibility | Bucketed per-object visibility / centered-time summary. | | hover_dwell | Hover hesitation summary for a mesh, with optional UV. | | compile_stall | Shader / pipeline / material compilation hitch duration. | | resource_sample | Opt-in low-rate GPU / memory footprint sample. | | capability_change | App-reported capability / fidelity fallback or recovery. | | asset_load | Asset name, bytes, load ms, time-to-first-frame. | | scene_change | Ordered marker for an active sceneId transition. | | viewport_resize | Debounced viewport/canvas size marker. | | visibility_change | Page visibility marker. | | focus_change | Window/canvas focus marker. | | context_lost | Rendering context lost marker. | | context_restored | Rendering context restored marker. | | graphics_diagnostic | Opt-in engine/GPU-health signal (errors, shader-compile failures, context loss, uncapturederror). Off by default. | | runtime_error | Opt-in JavaScript error / unhandled rejection capture. | | input_action | Discrete keyboard/gamepad/app action event. | | custom | Developer-defined name + open props record. |

Opt-in engine diagnostics (graphics_diagnostic)

graphics_diagnostic carries engine-authored GPU-health signals (ADR 0021 part 2): GPU errors/warnings, shader-compile/link failures, richer context-loss reasons, WebGPU uncapturederror, and sampled gl.getError(). It is a single engine-agnostic shape:

  • severity: info | warning | error | fatal
  • category: context-loss | validation | out-of-memory | shader-compile | device-lost | fallback
  • backend (optional): the producing API surface, reusing the graphics.api enum.
  • message / code (optional): length-capped free text; redact via beforeSend.
  • count (optional): rollup-or-marker discriminator — omit for a single discrete incident; set it to aggregate that many incidents into one per-session rollup (the cheap default so an error storm can't flood ingestion).
  • position (optional): best-effort camera world position for spatial diagnostic heatmaps.

Off by default. Capture is gated by the SDK's captureGraphicsDiagnostics flag (mirrors JS error capture). context_lost / context_restored are exempt and stay always-on. The fallback category is reserved for forward-compatibility and is not emitted by any connector (engine-driven fallback stays in capability_change).

Config contracts (not events)

A few shapes here are config / metadata, deliberately outside the event union — they are authored out-of-band and never reach the keyless ingest path:

  • sceneProxySchema — a scene's engine-agnostic proxy geometry (per-mesh AABBs) for the scene registry.
  • sceneRegionSchema / sceneRegionsSchema — named, labelled boxes that give a scene a vocabulary for where ({ id, label, bounds: [minX,minY,minZ,maxX,maxY,maxZ], description? }). Regions may overlap; sceneRegionsSchema bounds the set and rejects duplicate ids. Written with PUT /api/v1/scenes/:sceneId/regions and read back as the region=<id> query filter.
  • funnelStepSchema / funnelConfigSchema — closed, validated predicates over existing events.
  • annotationSchema / glossaryEntrySchema / savedAnalysisSchema (+ metadataAuthorKindSchema) — the project metadata a team leaves behind: a note on a spike, what a name means here, a question worth re-asking with its conclusion (ADR 0051 §5). They bound the collector's annotate-gated write path (/api/v1/annotations, /api/v1/glossary/:term, /api/v1/analyses). Events stay read-only: none of this reaches the ingest path or changes a captured event.

They are validated at the boundary like events; they just are not part of anyEventSchema.

Adding a new event type (extension point)

  1. Create src/events/myEvent.ts:

    import { z } from "zod";
    import { defineEvent } from "./defineEvent.js";
    
    export const myEventSchema = defineEvent("my_event", {
      someField: z.number(),
    });
    export type MyEvent = z.infer<typeof myEventSchema>;
  2. Register it in src/events/index.ts (eventSchemaList, anyEventSchema, eventSchemaByType) and re-export it.

  3. Add the literal "my_event" to EVENT_TYPES in src/constants.ts.

  4. Add a test in src/__tests__.

defineEvent automatically wires in the shared envelope and the type discriminant, so the union and all downstream exhaustiveness checks update from that single registration.

See also the repo-level add-event-type skill for threading a new event through the SDK, collector, storage, and replay.

Ingestion payload bounds

The collector's write endpoint (POST /api/v1/collect) is public and intentionally keyless (the cookieless, no-PII privacy model), so every free-text and collection field is bounded at the schema boundary. The caps live in src/limits.ts as the exported LIMITS constant and are shared by producers and the collector. An event that exceeds any cap fails validation, and the whole batch is rejected with 400.

| Bound | LIMITS key | Applies to | | -------------------------------- | ------------------------------------------------------------------------------------- | ----------------------- | | Events per batch | maxBatchEvents | collectRequest.events | | Project / session id length | maxProjectIdLength / maxSessionIdLength | envelope | | SDK version / URL length | maxSdkVersionLength / maxUrlLength | envelope | | Page title / referrer / lang | maxTitleLength / maxReferrerLength / maxLanguageLength | pageMeta | | Mesh / asset name length | maxMeshNameLength / maxAssetNameLength | mesh_*, asset_load | | Custom name / value / count | maxCustomNameLength / maxCustomPropValueLength / maxCustomPropEntries | custom | | User id / trait value / count | maxUserIdLength / maxUserTraitValueLength / maxUserTraitEntries | session_start.user | | Scene description / camera | maxSceneDescriptionLength / maxCameraNameLength | session_start.scene | | Scene-proxy mesh name/path/count | maxSceneProxyMeshNameLength / maxSceneProxyMeshPathLength / maxSceneProxyMeshes | sceneProxy | | Region label / description/count | maxSceneRegionLabelLength / maxSceneRegionDescriptionLength / maxSceneRegions | sceneRegion | | Node / bone / child path | maxNodeIdLength / maxBoneIdLength / maxChildPathLength | node_transform | | Diagnostic message / code | maxGraphicsDiagnosticMessageLength / maxGraphicsDiagnosticCodeLength | graphics_diagnostic |

Connectors must truncate locally rather than rely on rejection. A huge scene should not send an unbounded sceneProxy.meshes list and get the batch dropped: cap the list at LIMITS.maxSceneProxyMeshes (keep the largest / most-relevant meshes), and still report the true total in meshCount so the dashboard can show "N of M meshes". Likewise, clamp long mesh names, custom-prop values, and user traits before emitting. The schema caps are a safety net, not the primary mechanism.

Scripts

pnpm --filter @uptimizr/schema build
pnpm --filter @uptimizr/schema typecheck
pnpm --filter @uptimizr/schema test

Licensed under Apache-2.0.