@saaspro/shared

v0.4.0

Published

Shared event contract, zod schemas and helpers for SaaS Pro SDKs and ingest.

Readme

@saaspro/shared

The event contract shared by every SaaS Pro SDK and by the ingest pipeline itself: zod schemas, TypeScript types, source classification, a small user-agent parser and the hashing helpers the platform relies on.

You rarely install this directly — @saaspro/browser, @saaspro/node and @saaspro/react already depend on it. Install it when you build your own transport, validate envelopes server-side, or implement the connector contract.

npm install @saaspro/shared

Event contract

import { ingestEnvelopeSchema, type IngestEnvelope } from "@saaspro/shared";

const parsed = ingestEnvelopeSchema.safeParse(await request.json());
if (!parsed.success) return Response.json({ error: "invalid envelope" }, { status: 400 });

| Export | What it is | | --- | --- | | ingestEnvelopeSchema | { key?, sentAt, sdk: { name, version }, events[] }, 1–100 events | | ingestEventSchema | Discriminated union over the eight event types | | pageEventSchema trackEventSchema identifyEventSchema aliasEventSchema errorEventSchema metricEventSchema vitalEventSchema sessionEventSchema | One schema per type | | eventContextSchema utmSchema clickIdsSchema attributionSchema touchSchema stackFrameSchema breadcrumbSchema | Sub-schemas | | flagsEvaluateRequestSchema | Body of POST /api/v1/flags/evaluate | | LIMITS | Batch size, name length, property bytes, breadcrumbs, stack length, label keys | | RESERVED_EVENT_NAMES | $pageview, $pageleave, $click, $outbound, $form_submit, $web_vital, $session_end, $error, $identify, $alias | | RESERVED_TRAITS | email, name, firstName, lastName, phone, avatar, plan, company, createdAt |

Limits enforced by the schemas:

  • 1–100 events per envelope
  • event names ≤ 120 characters, matching ^[\w $.:/-]+$ (trimmed)
  • properties / traits / extra ≤ 16 KB serialized
  • ≤ 100 breadcrumbs, ≤ 32 KB of stack text, ≤ 20 metric label keys

Attribution

import { classifySource, normalizeDomain } from "@saaspro/shared";

classifySource("www.google.com", { source: "google", medium: "cpc" }, { gclid: "abc" });
// → { source: "search", medium: "cpc", channel: "Paid Search" }

classifySource("chatgpt.com");
// → { source: "ai", medium: null, channel: "AI Assistant" }

classifySource(null);
// → { source: "direct", medium: null, channel: "Direct" }

source is one of direct | search | social | email | paid | referral | ai | unknown. Precedence: ad click ids → utm_medium → utm_source → referrer domain → direct. A paid signal never erases the platform — gclid on a Google referrer stays search with the channel Paid Search; the bare paid source is only used when the platform cannot be determined.

User agents

import { isBotUserAgent, parseUserAgent } from "@saaspro/shared";

parseUserAgent(navigator.userAgent);
// → { browser: "Chrome", browserVersion: "141.0.0.0", os: "macOS", osVersion: "10.15.7", device: "desktop" }

isBotUserAgent("Mozilla/5.0 (compatible; GPTBot/1.2; +https://openai.com/gptbot)"); // true

device is desktop | mobile | tablet | bot | unknown. The parser returns null rather than guessing, and bots short-circuit to device: "bot".

Identity helpers

import { hashEmail, normalizeEmail, normalizePhone, sha256Hex } from "@saaspro/shared";

normalizeEmail(" [email protected] ");   // "[email protected]"
normalizePhone("+1 (415) 555-2671");     // "14155552671"  (E.164 digits, no leading +)
await sha256Hex("[email protected]");    // lowercase hex
await hashEmail(" [email protected] ");  // normalize + hash in one call

sha256Hex prefers WebCrypto and falls back to node:crypto through a dynamic import, so bundlers targeting the browser never need to resolve it.

Keys and flags

import { isPublicKey, isSecretKey, parseIngestKey, murmurhash3_32, rolloutBucket } from "@saaspro/shared";

isPublicKey("spm_pub_prod_abcdefghijklmnopqrstuv"); // true
parseIngestKey("spm_pub_prod_abcdefghijklmnopqrstuv");
// → { kind: "public", environment: "prod", prefix: "spm_pub_prod", last4: "stuv" }

rolloutBucket("new-onboarding", "user_42") < 25; // in the 25% rollout?

murmurhash3_32(str, seed?) is the MurmurHash3 x86 32-bit function used for deterministic flag bucketing across the browser SDK, the Node SDK and the evaluation endpoint.

Connector types

Types and helpers for the signed tenant connector (implemented by @saaspro/node's createConnectorHandler): ServiceClaims, ConnectorUser, ConnectorUserDossier, ConnectorControl, ConnectorHandlers, ConnectorRequestContext, CONNECTOR_CAPABILITIES, CONNECTOR_ROUTE_PERMISSIONS and redactUser(user, permissions).

import { redactUser } from "@saaspro/shared";

redactUser({ id: "u1", email: "[email protected]", name: "Ada", createdAt }, permissions);
// email and name are dropped unless permissions includes "people.pii.read"

Constants

SDK_VERSION, DEFAULT_HOST (https://saaspro.dev), INGEST_PATH (/api/v1/ingest), FLAGS_PATH (/api/v1/flags/evaluate), STORAGE_KEYS, CLICK_ID_PARAMS, UTM_PARAMS, VITAL_THRESHOLDS and rateVital(name, value).

Transport utilities

Transport utilities: utf8Bytes(text), batchByBytes(events, maxCount, maxBytes, overhead) and retryAfterMs(header, now?). Batching preserves event IDs and order and returns { batches, dropped }; include the serialized empty envelope in overhead. Retry-After returns milliseconds or null for invalid input. SDK-facing event DTOs are explicit types so browser/Node consumers do not need to install Zod merely to typecheck; schema consumers still use Zod here.

License

MIT