Docs

React + Vite

Also applies to other single-page apps (Vue, Svelte, Solid) – only the entry file differs.

Install

npm install {{API_ORIGIN}}/v1/sdk/3x-engine-sdk-0.1.0.tgz
.env
VITE_X3_KEY={{PUBLISHABLE_KEY}}
VITE_X3_HOST={{API_ORIGIN}}
src/main.tsx
import { init } from "@3x-engine/sdk";
import { forms, sections, video } from "@3x-engine/sdk/plugins";

init({
  key: import.meta.env.VITE_X3_KEY,
  host: import.meta.env.VITE_X3_HOST,
  plugins: [video(), sections(), forms()],
});

// …then createRoot(…).render(…) as before

Call init before rendering. React Router, TanStack Router and other History-API routers produce spa pageviews automatically; for hash routers add hashRouting: true.

Verify

  1. npm run dev, open the app, navigate between two routes.
  2. POST {{API_ORIGIN}}/v1/e returns 204; the second route is an spa pageview of the same session.
  3. Watch the events arrive on the dashboard's install page.

Vite's dev server runs on http://localhost:5173 – enable Allow localhost in the project's tracking settings for local testing.

Proxy (optional)

Static SPAs need the proxy at the hosting level. On Vercel, a serverless function:

.env
X3_API_ORIGIN={{API_ORIGIN}}
{{PROXY_KEY_ENV}}=sk_3x_YOUR_PROXY_KEY
api/n3/[...path].ts
import { createX3ProxyHandler } from "@3x-engine/sdk/next/proxy";

const handler = createX3ProxyHandler({ apiOrigin: process.env.X3_API_ORIGIN!, proxyKey: process.env.{{PROXY_KEY_ENV}} });

export const config = { runtime: "edge" };

export default function proxy(request: Request): Promise<Response> | Response {
  const method = request.method as "GET" | "POST" | "OPTIONS";
  return handler[method] ? handler[method](request) : new Response(null, { status: 405 });
}
vercel.json
{ "rewrites": [{ "source": "{{PROXY_PATH}}/:path*", "destination": "/api/n3/:path*" }] }
.env
VITE_X3_HOST={{PROXY_PATH}}

Other hosts: Netlify, Cloudflare Worker, Nginx. For local development a Vite dev-server proxy is enough:

vite.config.ts
export default defineConfig({
  server: { proxy: { "{{PROXY_PATH}}": { target: "{{API_ORIGIN}}", changeOrigin: true, rewrite: (path) => path.replace(/^\{{PROXY_PATH}}/, "") } } },
});
src/ConsentBanner.tsx
import { consent } from "@3x-engine/sdk";

export function ConsentBanner({ onDone }: { onDone: () => void }) {
  return (
    <div role="dialog" aria-label="Cookie consent">
      <button onClick={() => { consent("granted"); onDone(); }}>Accept</button>
      <button onClick={() => { consent("denied"); onDone(); }}>Decline</button>
    </div>
  );
}

Events

src/PlanPicker.tsx
import { track } from "@3x-engine/sdk";

export function PlanPicker() {
  return (
    <select onChange={(event) => track("plan_selected", { plan: event.currentTarget.value })}>
      <option value="starter">Starter</option>
      <option value="pro">Pro</option>
    </select>
  );
}

Register plan_selected with an enum schema to keep its values clean (Custom events).