Introduction
Prabisha Analytics is a privacy-first, developer-focused analytics service. You instrument your frontend with the vyzora-sdk, events are ingested into our high-performance Express backend, and your dashboard queries real-time aggregated data from a PostgreSQL database — zero sampling, zero third-party tracking.
Unlike standard analytics platforms, Prabisha Analytics is designed specifically for modern web applications. Every event is validated server-side using Zod and stored using the Prisma ORM in a secure, optimized schema. There is no data sharing with any external advertising service.
Technical Architecture
Quick Start
Minimum working setup in three steps.
1. Install
npm install vyzora-sdk2. Initialize
import { Vyzora } from 'vyzora-sdk';
new Vyzora({
apiKey: 'your_project_api_key',
enabled: true,
});3. Track
vyzora.track('button_click', { plan: 'pro' });enabled is false or omitted, the SDK exits the constructor immediately. No queue, no listeners, no timers are created. Nothing executes.Installation
Simple Script Tag (Recommended)
Just add one line to your HTML <head> tag:
<script src="https://api.prabisha.com/sdk/browser.js"
data-api-key="your_project_api_key"
data-enabled="true"></script>That's it! The SDK auto-initializes and starts tracking. Use window.vyzora.track() for custom events.
npm / yarn / pnpm
npm install vyzora-sdk
yarn add vyzora-sdk
pnpm add vyzora-sdkNext.js (App Router)
Option 1: Using Next.js Script component (recommended)
// app/layout.tsx
import Script from 'next/script';
export default function RootLayout({ children }) {
return (
<html>
<body>
<Script
src="https://api.prabisha.com/sdk/browser.js"
data-api-key={process.env.NEXT_PUBLIC_VYZORA_KEY}
data-enabled="true"
strategy="afterInteractive"
/>
{children}
</body>
</html>
);
}Option 2: Using a Provider component
// components/VyzoraProvider.tsx
'use client';
import { useEffect } from 'react';
import { Vyzora } from 'vyzora-sdk';
export default function VyzoraProvider() {
useEffect(() => {
new Vyzora({
apiKey: process.env.NEXT_PUBLIC_VYZORA_KEY!,
enabled: true,
});
}, []);
return null;
}
// app/layout.tsx
import VyzoraProvider from '@/components/VyzoraProvider';
export default function RootLayout({ children }) {
return <html><body><VyzoraProvider />{children}</body></html>;
}React (Vite / CRA)
// src/main.tsx
import { Vyzora } from 'vyzora-sdk';
new Vyzora({ apiKey: import.meta.env.VITE_VYZORA_KEY, enabled: true });Vanilla JS / HTML
<!-- Simple one-liner -->
<script src="https://api.prabisha.com/sdk/browser.js"
data-api-key="your_key"
data-enabled="true"></!-->
<!-- Track custom events -->
<button onclick="window.vyzora.track('signup_click')">
Sign Up
</button>The SDK must run in the browser. It guards against typeof window === 'undefined' and exits silently when executed in a Node.js or Edge runtime context.
Initialization
The Vyzora constructor accepts a config object. Only apiKey is required.
const vyzora = new Vyzora({
apiKey: 'your_project_api_key', // Required
enabled: true, // Default: false
batchSize: 20, // Default: 20
flushInterval: 10000, // Default: 10000ms (10s)
debug: false, // Default: false
});If apiKey is missing, the constructor throws synchronously: [Vyzora] apiKey is required. — catch this at your integration point.
The apiKey is mandatory for every instance. You can find it in your project settings on the Prabisha Analytics dashboard.
Tracking Events
vyzora.track('button_click', { plan: 'pro', source: 'hero' });track(eventType, metadata?) builds an event and pushes it into the in-memory queue.
What the SDK attaches automatically
sessionIdCurrent session UUID from localStorage (vyzora_sid)visitorIdStable visitor UUID from localStorage (vyzora_vid)pathwindow.location.pathname + window.location.search at call timebrowser / os / deviceTypeUA-parsed from navigator.userAgentscreenWidth / screenHeightFrom window.screenlanguagenavigator.languagereferrerdocument.referrer (omitted if empty)timezoneIntl.DateTimeFormat().resolvedOptions().timeZoneYour custom metadata is merged on top of the auto-collected metadata. Your values overwrite automatic ones on key collision. If the merged object is empty, metadata is omitted from the payload entirely.
track() is wrapped in try/catch. It will never throw. If the SDK is not enabled or the queue is not initialized, it no-ops.
Pageview Tracking (SPA Behavior)
Pageview tracking is automatic. The SDK hooks into browser navigation APIs on initialization. No manual calls are needed for standard navigation.
Triggers
window load eventFires once. Records the initial pageview when the page finishes loading.history.pushStateWrapped. Fires a pageview after every programmatic navigation (React Router, Next.js Link).history.replaceStateWrapped. Fires a pageview on replace navigations.window popstateListened. Fires on browser back/forward button presses.Deduplication
The SDK stores the last tracked path as pathname + search. If the new path matches, the pageview is dropped. Hash-only changes (e.g. #section) are never tracked because they do not change pathname or search.
Singleton Guard
pushState and replaceState are wrapped at most once (guarded by an instance-level historyWrapped flag). Re-instantiating the SDK will not double-wrap history methods.
Manual override
vyzora.pageview('/custom-path'); // explicit path
vyzora.pageview(); // defaults to current pathname + searchVisitor & Session Model
Visitor ID
Session ID
identify()
vyzora.identify('user_db_id_123');Overrides the auto-generated visitor ID with a known user identity for all subsequent events. Does not modify localStorage. The override is stored in-memory on the Vyzora instance and is lost on page unload.
Batching & Delivery
In-memory Queue
Events are pushed into an in-memory array (the Queue class). Nothing is written to disk. If the page unloads before a flush, un-sent events are lost unless sendBeacon fires.
Flush Triggers
IntervalAuto-flush every flushInterval ms (default: 10 000ms). Interval starts on SDK init. Only one interval exists — guarded by a null check.Batch sizeIf queue.length >= batchSize (default: 20), an immediate flush() is triggered after push().visibilitychangeFlushed when document.visibilityState becomes "hidden" (tab switch, minimize).pagehideFlushed when the pagehide window event fires (navigation away, page unload).destroy()Explicit flush on SDK teardown, then clears interval and removes listeners.Transport
On flush, the SDK first attempts navigator.sendBeacon. Beacon is preferred because the browser guarantees it fires even after the page closes. If sendBeacon is unavailable or returns false, the SDK falls back to fetch(keepalive: true).
Retry Policy
5xx responseSingle retry after 2 000ms. No second retry.Network errorSingle retry after 2 000ms. No second retry.4xx response (401, 403, 429, etc.)Dropped silently. No retry.Retry failureDropped silently. Events are not re-inserted into the queue.Race Condition Guard
A flushing boolean on the Queue class prevents concurrent flush calls. If two flushes are triggered simultaneously (e.g.,visibilitychange + pagehide), the second returns immediately. The flag resets in a try/finally block, so it always resets even if sendBatch throws.
Reliability Guarantees
SDK never throws to host app
All public methods (track, pageview, flush, destroy) are wrapped in try/catch. Errors are swallowed internally.
No console.error pollution
Errors fail silently. console.log output only appears when debug: true.
No execution when disabled
enabled: false causes the constructor to return before creating any queue, timers, or event listeners.
Zero global pollution
The SDK does not assign anything to window.* and does not modify any prototype chain.
History wrapping only
The only mutation is overriding history.pushState and history.replaceState. This is done at most once per SDK instance.
Safe localStorage access
All reads/writes go through safeGet, safeSet, safeRemove wrappers. Safari private mode (SecurityError) and QuotaExceededError are caught silently.
In-memory visitor fallback
If localStorage is unavailable, a module-level fallbackId is used. Visitor ID is stable for the page lifetime.
Safe destroy()
destroy() clears the interval, removes event listeners, and flushes remaining events. Safe to call multiple times.
Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
apiKey | string | Required | Your project API key from the Prabisha Analytics dashboard. Throws if missing. |
enabled | boolean | false | Must be true to activate tracking. The SDK is completely inert if false. |
batchSize | number | 20 | Max events per batch. Triggers an immediate flush when reached. |
flushInterval | number | 10000 | Auto-flush interval in milliseconds. |
debug | boolean | false | Enables verbose console.log output from the SDK internals. |
API Reference
new Vyzora(config)Creates the SDK instance. Throws synchronously if apiKey is missing. If enabled is false, returns immediately. If window is undefined (SSR), returns immediately.
Parameters
configVyzoraConfigSee Configuration Options tablevyzora.track(eventType, metadata?)Queues a custom event. Auto-collects browser metadata and merges it with your metadata. Path is taken from window.location. No-ops if not enabled.
Parameters
eventTypestringA short identifying string, e.g. "button_click"metadataRecord<string, unknown>Optional custom key/value pairs. Merged with auto-metadata.vyzora.pageview(path?)Records a pageview event. Deduplicates against the last tracked path. If path is omitted, uses window.location.pathname + window.location.search.
Parameters
pathstring (optional)Explicit path to record. Defaults to current URL.vyzora.identify(visitorId)Overrides the auto-generated visitor ID for all subsequent events on this instance. Does not persist to localStorage.
Parameters
visitorIdstringA known user identifier, e.g. a database user ID.vyzora.flush()Manually triggers an immediate flush of the event queue. Returns a Promise that resolves when the transport completes.
vyzora.resetSession()Removes vyzora_sid and vyzora_session_ts from localStorage. The next track() or pageview() call will generate a new session ID.
vyzora.destroy()Flushes remaining events, clears the interval timer, and removes visibilitychange and pagehide listeners. Call in component cleanup hooks or SPA teardown.
Backend Ingestion Format
The SDK posts a JSON payload to POST /api/ingest. This is the exact shape our backend expects and validates:
// POST /api/ingest
// Content-Type: application/json
{
"apiKey": "64-char-hex-string",
"events": [
{
"sessionId": "uuid-v4",
"visitorId": "uuid-v4",
"eventType": "pageview",
"path": "/pricing",
"metadata": {
"browser": "Chrome",
"browserVersion": "121",
"os": "macOS",
"deviceType": "desktop",
"screenWidth": 1440,
"screenHeight": 900,
"language": "en-US",
"referrer": "https://google.com",
"timezone": "Asia/Kolkata"
}
}
]
}Data Constraints (Zod Enforced)
sessionId≤ 128 chars, requiredvisitorId≤ 128 chars, requiredeventType≤ 64 chars, requiredpath≤ 512 chars, requiredmetadataJSON object, optionalapiKeyValidated against Database Project table.The backend server also automatically captures ipAddressand userAgent for server-side parsing. These fields are never transmitted by the client SDK to reduce payload weight.
Security & Validation
API Key Validation
Every ingest request must include a valid 64-character API key. The backend verifies this against the PostgreSQL database before performing any ingestion logic.
No Client-side Trust
We never trust client-provided IDs for authorization. The API key is the sole source of authentication for incoming event streams.
Rate Limiting
Our infrastructure enforces strict rate limits via express-rate-limit on all endpoints to prevent abuse and ensure stability.
Encrypted Sessions
Dashboard login is managed via secure, HttpOnly, SameSite=None JWT cookies, ensuring tamper-proof authenticated access.
Data Residency
All event data is stored in a hardened PostgreSQL instance using Prisma for type-safe database queries and automated migrations.
Project Isolation
Every metrics query validates project ownership at the database level using relational JOINs before returning aggregated results.
Architecture & Development
Prabisha Analytics is built using a modern, scalable stack designed for high-throughput data ingestion and low-latency metrics visualization.
# Tech Stack Visualization
Runtime SDK: TypeScript (tsup bundle)
API Backend: Express.js (Node.js)
Database: PostgreSQL (Prisma ORM)
Dashboard: Next.js 16 (App Router)Backend Logic
The core logic resides in a centralized Express.js application that handles routing, Zod-based validation, and PostgreSQL communication. We use Prisma for efficient query building and schema management.
Frontend Stack
The dashboard is a high-performance Next.js 16 application utilizing server components for initial rendering and React Query for real-time metric updates.
Service Reliability
Stability is our priority. Our core transport layer utilizes asendBeacon first approach, falling back to high-performance fetch with keepalive parity to ensure events are delivery even during page unloads.
FAQ
Why are events not appearing in my dashboard?
Check: (1) enabled is true, (2) the API key is correct, (3) the endpoint is set to your production backend, (4) the backend is running and reachable. Open DevTools Network and look for POST /api/ingest — a 401 means API key is invalid or the project was deleted.
Why am I seeing duplicate pageviews?
Likely caused by SDK instantiation running more than once in the same page lifecycle (e.g. React Strict Mode double-invoke in development). In production, React Strict Mode does not double-invoke effects. The historyWrapped guard ensures pushState/replaceState are wrapped at most once per instance, but two separate Vyzora instances would each wrap independently.
How do I disable tracking for specific users?
Pass enabled: false when constructing the SDK instance. All public methods no-op immediately. Alternatively, call vyzora.destroy() to tear down an already-running instance.
Does the SDK work in SSR (Next.js server components)?
You can import the SDK in SSR contexts. The constructor checks typeof window === "undefined" and returns early if true. No queue, listeners, or timers are created on the server. Wrap initialization in a useEffect or mount it in a client component.
Does it work without localStorage?
Yes. Storage access is wrapped in try/catch. If localStorage is unavailable (Safari private mode, SecurityError), visitor ID falls back to a stable in-memory variable for the page lifetime, and session tracking degrades gracefully.
What happens if I delete a project that is still sending events?
The API key is immediately invalid. Every subsequent POST /api/ingest with that key returns 401 Unauthorized. No events are written. The SDK will retry once on network errors, but 401 is never retried — those batches are dropped silently.
Troubleshooting
Debugging with DevTools
Initialize with debug: true to enable verbose SDK logging. Open the Network tab and filter for ingest to see every POST request, its status code, and timing.
401 on POST /api/ingest
- —The API key does not match any project in the database.
- —The project was deleted.
- —You are testing with a local API key against a production backend (or vice versa).
- —The SDK was built with the wrong VYZORA_API_URL. Rebuild the SDK and redeploy.
CORS errors on ingest
- —The backend CORS origin does not match the frontend domain exactly (including protocol and port).
- —FRONTEND_URL is missing or set to the wrong value on the backend server.
- —credentials: true must be in the CORS config if cookies are used.
Auth loop / not redirecting after GitHub login
- —GitHub OAuth App callback URL does not match BACKEND_URL/api/auth/github/callback.
- —FRONTEND_URL is wrong — the post-OAuth redirect goes to the wrong domain.
- —NODE_ENV is not set to production — cookie SameSite policy defaults to lax instead of none.
Events sent but not in dashboard
- —Verify POST /api/ingest returns 200 in Network tab.
- —Check the Prisma database directly: look at the Event table for recent rows with the correct projectId.
- —Dashboard metrics queries are time-range filtered (7d default). Check if events are within range.