Web SDK

Client-side event tracking for browsers with @vanta-dev/web.

The Web SDK tracks events from the browser. It is a light wrapper around the Vanta client that loads once per page and automatically attaches client context to everything it sends.

Installation

bash
npm install @vanta-dev/web

Setup

Create a client with a browser-safe API key and a source label. The source option identifies where events originate.

typescript
import { Vanta } from '@vanta-dev/web'

const vanta = new Vanta({
  apiKey: 'vk_...',
  source: 'web'
})

Tracking events

Page views

The web SDK tracks a page view by recording an event and passing the current path.

typescript
vanta.track({
  event: 'page.view',
  data: {
    path: window.location.pathname
  }
})

Custom events

Any other interaction is tracked the same way, with your own event name and data.

typescript
button.addEventListener('click', () => {
  vanta.track({
    event: 'button.click',
    data: {
      label: 'upgrade',
      page: '/pricing'
    }
  })
})

Automatic context

The web SDK captures browser context automatically for every event. You do not need to collect or pass this data yourself.

  • Browser and browser version
  • Operating system
  • Device type and model
  • Screen and viewport size
  • Current URL, path, and referrer
  • Timezone

Buffering

The web SDK buffers events in memory and flushes them in batches. This reduces the number of network requests and survives brief connectivity gaps.

Events are delivered on a flush cycle. Because delivery is asynchronous, do not rely on an event being visible immediately after calling track.

Bypassing ad blockers with a proxy

Ad blockers often block analytics endpoints, which can silently stop page views and events from being delivered. To keep tracking reliable, route Vanta requests through your own backend on the same origin as your app. Ad blockers cannot block a request to your own domain, so page views survive for users with ad blockers installed.

Custom API route

Create a proxy route in your app that forwards Vanta requests to the Vanta API. This example is an App Router route handler that proxies any collection path.

typescript
import { API_URL } from "@/lib/constants"

const VANTA_API_KEY = process.env.VANTA_API_KEY

const FORWARDED_HEADERS = [
  'accept-language',
  'user-agent',
  'referer',
  'origin',
  'x-forwarded-for',
]

async function proxyRequest(request: Request, path: string) {
  try {
    const url = `${API_URL}/${path}${new URL(request.url).search}`
    const headers = new Headers()
    headers.set('content-type', request.headers.get('content-type') || 'application/json')
    headers.set('authorization', `Bearer ${VANTA_API_KEY}`)

    const projectId = request.headers.get('x-project-id')
    if (projectId) headers.set('x-project-id', projectId)

    for (const name of FORWARDED_HEADERS) {
      const value = request.headers.get(name)
      if (value) headers.set(name, value)
    }

    const init: RequestInit = {
      method: request.method,
      headers,
    }

    if (request.method !== 'GET' && request.method !== 'HEAD') {
      init.body = await request.text()
    }

    const res = await fetch(url, init)
    const body = await res.text()

    return new Response(body, {
      status: res.status,
      headers: { 'content-type': res.headers.get('content-type') || 'application/json' },
    })
  } catch (err) {
    console.error('PROXY ERROR:', err)
    return new Response(JSON.stringify({ error: String(err) }), { status: 500 })
  }
}

export async function GET(
  request: Request,
  { params }: { params: Promise<{ path: string[] }> },
) {
  const { path } = await params
  return proxyRequest(request, path.join('/'))
}

export async function POST(
  request: Request,
  { params }: { params: Promise<{ path: string[] }> },
) {
  const { path } = await params
  return proxyRequest(request, path.join('/'))
}

export async function PATCH(
  request: Request,
  { params }: { params: Promise<{ path: string[] }> },
) {
  const { path } = await params
  return proxyRequest(request, path.join('/'))
}

export async function DELETE(
  request: Request,
  { params }: { params: Promise<{ path: string[] }> },
) {
  const { path } = await params
  return proxyRequest(request, path.join('/'))
}

Point the client at the proxy

Configure the Vanta client with the proxy URL. The apiUrl option replaces the default Vanta endpoint, so all events are sent to your own origin instead.

tsx
"use client";

import {
  createContext,
  useContext,
  useEffect,
  useRef,
  type ReactNode,
} from "react";
import { usePathname } from "next/navigation";
import { Vanta } from "@vanta-dev/web";

const apiKey = process.env.NEXT_PUBLIC_VANTA_API_KEY;

if (!apiKey) {
  throw new Error("NEXT_PUBLIC_VANTA_API_KEY is not configured");
}

const client = new Vanta({
  apiKey,
  apiUrl: "/api/collect",
  flushInterval: 15000,
  flushSize: 10,
});

const VantaContext = createContext<Vanta>(client);

export function useVanta() {
  return useContext(VantaContext);
}

export function VantaProvider({ children }: { children: ReactNode }) {
  const pathname = usePathname();
  const trackedPathname = useRef<string | null>(null);

  useEffect(() => {
    if (trackedPathname.current === pathname) return;

    trackedPathname.current = pathname;
    client.page().catch((err) => {
      console.log("Failed to track page view", err);
      client.track({
        event: "error",
        data: {
          message: "Failed to track page view",
          error: err instanceof Error ? err.message : String(err),
          pathname,
        },
      });
    });
  }, [pathname]);

  return (
    <VantaContext.Provider value={client}>{children}</VantaContext.Provider>
  );
}

The provider tracks a page view whenever the pathname changes, deduplicated so it only fires once per route. Because the client points at /api/collect, the requests go through your proxy instead of straight to Vanta.

Best practices

  • Create one Vanta client per page and reuse it.
  • Never embed a server-side API key in browser code; use a browser-safe key.
  • Keep event payloads small and focused on what you will query later.
  • Track meaningful user actions rather than every state change.