@saaspro/shared
v0.4.0
Published
Shared event contract, zod schemas and helpers for SaaS Pro SDKs and ingest.
Maintainers
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/sharedEvent 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)"); // truedevice 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 callsha256Hex 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