Prabisha Analytics Logo
Prabisha Analytics

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

vyzora-sdkCollects events in browser → batches → flushes to Express API
Express BackendValidates API key → authenticated write via Prisma ORM
PostgreSQLHigh-performance storage with composite indexing on (projectId, createdAt)
Next.js DashboardGitHub OAuth login → Real-time SQL aggregation metrics UI

Quick Start

Minimum working setup in three steps.

1. Install

npm install vyzora-sdk

2. Initialize

import { Vyzora } from 'vyzora-sdk';

new Vyzora({
  apiKey: 'your_project_api_key',
  enabled: true,
});

3. Track

vyzora.track('button_click', { plan: 'pro' });
Critical: If 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-sdk

Next.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 time
browser / os / deviceTypeUA-parsed from navigator.userAgent
screenWidth / screenHeightFrom window.screen
languagenavigator.language
referrerdocument.referrer (omitted if empty)
timezoneIntl.DateTimeFormat().resolvedOptions().timeZone

Your 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 + search

Visitor & Session Model

Visitor ID

Storage keyvyzora_vid (localStorage)
RotationNever. Persists across browser sessions.
Generationcrypto.randomUUID() with Math.random() fallback
Storage failureFalls back to a stable module-level in-memory variable for the page lifetime. New on next hard load.

Session ID

Storage keyvyzora_sid (localStorage)
Timestamp keyvyzora_session_ts (localStorage)
RotationAfter 30 minutes of inactivity (no track() calls)
Timestamp updateOn every getSessionId() call — including track() and pageview()
Manual resetvyzora.resetSession() — removes both keys. New session on next event.

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

OptionTypeDefaultDescription
apiKeystringRequiredYour project API key from the Prabisha Analytics dashboard. Throws if missing.
enabledbooleanfalseMust be true to activate tracking. The SDK is completely inert if false.
batchSizenumber20Max events per batch. Triggers an immediate flush when reached.
flushIntervalnumber10000Auto-flush interval in milliseconds.
debugbooleanfalseEnables 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 table
vyzora.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, required
visitorId≤ 128 chars, required
eventType≤ 64 chars, required
path≤ 512 chars, required
metadataJSON object, optional
apiKeyValidated 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.

Note: We monitor our ingestion endpoints 24/7. Our backend is horizontally scalable to handle sudden bursts of event traffic without increasing ingestion latency.

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.