@hellyeah/x-ray

Multi-platform analytics SDK for browser, React, Next.js, Remix, Vue, Svelte, Nuxt, Astro, and server environments
20,021
Install
npm install @hellyeah/x-ray
Latest Version:1.2.0
Vue:3
License:MIT
Last Updated:Jun 5, 2026
Links: npm
Maintainer: 0xhellyeah

X-Ray Analytics

Overview

@hellyeah/x-ray allows you to track page views, custom events, and user identity across any JavaScript framework or server environment.

All page views are automatically tracked by default. Custom events and user identification are available via a simple API.

Quickstart

  1. Install the package

    npm install @hellyeah/x-ray
    
  2. Add analytics to your app

    Next.js

    import { Analytics } from "@hellyeah/x-ray/next";
    
    export default function RootLayout({ children }) {
      return (
        <html>
          <body>
            {children}
           <Analytics
             websiteId="your-website-id"
             env="staging"
           />
          </body>
        </html>
      );
    }
    

    React

    import { Analytics } from "@hellyeah/x-ray/react";
    
    function App() {
      return (
        <>
          <Analytics websiteId="your-website-id" />
          {/* your app */}
        </>
      );
    }
    

    Vue / Nuxt / Svelte / Remix / Astro — see Framework Guides below.

    Vanilla JS

    import { inject } from "@hellyeah/x-ray";
    
    inject({ websiteId: "your-website-id" });
    

    Server (Node.js / Bun)

    import { XRay } from "@hellyeah/x-ray/server";
    
    const xray = new XRay("your-website-id", { env: "prod" });
    xray.track("signup", { distinctId: "user_42" });
    
    // Before process exit, flush remaining events
    await xray.shutdown();
    

    Serverless / Edge (Vercel, AWS Lambda, Cloudflare Workers)

    In short-lived environments, set flushAt: 1 and flushInterval: 0 so events are sent immediately rather than batched:

    import { XRay } from "@hellyeah/x-ray/server";
    
    const xray = new XRay("your-website-id", {
      flushAt: 1,
      flushInterval: 0,
    });
    
    xray.track("signup", { distinctId: "user_42" });
    

    Or use trackImmediate to bypass the queue entirely and await delivery:

    await xray.trackImmediate("signup", { distinctId: "user_42" });
    
  3. Deploy your app and see data flowing in.

Environment tags

Every browser and server event carries an env tag. The default is "prod". Use a short value such as "staging", "preview", or "pr-123" when you want non-production traffic separated from production analytics.

<Analytics websiteId="your-website-id" env="staging" />
const xray = new XRay("your-website-id", { env: "staging" });

env must be 1-64 characters after trimming and may contain only letters, numbers, dots, underscores, and hyphens. The reserved values none, null, undefined, true, and false are rejected case-insensitively. Invalid values fall back to "prod" with one process-level warning.

Do not use mode to separate local, staging, preview, or production traffic. Use env. The mode option, disabled option, HELLYEAH_TRACKER_MODE, and NEXT_PUBLIC_HELLYEAH_TRACKER_MODE are deprecated, ignored, and will be removed in v2.

If websiteId is missing or blank, the SDK no-ops and warns once so analytics misconfiguration does not break the host app. This is a fail-soft safety path, not the recommended disable mechanism.

Resolution order:

Surface Option Env var Default
Next.js browser env NEXT_PUBLIC_HELLYEAH_TRACKER_ENV "prod"
Server env HELLYEAH_TRACKER_ENV "prod"

NEXT_PUBLIC_* values are bundled at build time by Next.js. Changing NEXT_PUBLIC_HELLYEAH_TRACKER_ENV requires a rebuild and redeploy. For Vite, Astro, Nuxt, and other browser builds, pass env from that framework's own env mechanism. For runtime control, pass the prop explicitly.

Breaking changes in 1.0

XRAY_WEBSITE_ID and XRAY_HOST are no longer read by the server SDK. Pass websiteId as the first constructor argument and host in options:

const xray = new XRay("your-website-id", {
  host: "https://xray.hellyeahai.com",
});

Frameworks

Framework Import path Usage
Next.js @hellyeah/x-ray/next <Analytics /> component with Suspense boundary
React @hellyeah/x-ray/react <Analytics /> component
Remix @hellyeah/x-ray/remix <Analytics /> component
Vue @hellyeah/x-ray/vue <Analytics /> component
Svelte @hellyeah/x-ray/svelte injectAnalytics() helper
Nuxt @hellyeah/x-ray/nuxt injectAnalytics() + <Analytics />
Astro @hellyeah/x-ray/astro Re-exports inject and track
Server @hellyeah/x-ray/server XRay class with batching, retry, and immediate send
Vanilla @hellyeah/x-ray inject(), track(), identify(), pageview()
Browser @hellyeah/x-ray/browser Browser helpers without React or framework imports

All browser and framework entrypoints re-export track, identify, getCookies, getVisitorId, and ready. Use @hellyeah/x-ray/server only for server-side XRay.

Custom Events

import { track } from "@hellyeah/x-ray";

track("signup", { plan: "pro" });

For the full list of predefined conversion events and ad platform mappings, see the Event Catalog.

User Identification

import { identify } from "@hellyeah/x-ray";

identify("user_42", { name: "Jane" });

For Enhanced Conversions (ad platform server-to-server attribution), pass email and/or phone to the browser identify():

identify("user_42", { email: "jane@example.com", phone: "+15555551234" });

The remote tracker emits an identify item on the next batch; the server hashes the values with SHA-256 before storing them on the session.

Server-side identity is a different shape — see Server-side identity.

Cookie consent

The browser tracker writes a first-party hy_attr cookie (base64url JSON, 90-day TTL) that captures visitor ID and attribution click IDs across page loads. Control it via <Analytics> props:

<Analytics
  websiteId="your-website-id"
  cookies={false}              // default: true (cookie enabled)
  cookieDomain=".example.com"  // share across subdomains
  debug={true}                 // optional local browser warnings
/>
Prop Type Default Meaning
cookies boolean true Set false to disable the cookie (e.g. pre-consent).
cookieDomain string host-only Scope the cookie, e.g. .example.com to share subdomains.
debug boolean false Emit browser warnings for setup/debugging issues.
onReady () => void — Runs when the remote tracker script has loaded.

Consent helpers

Toggle the cookie at runtime after the user grants or revokes consent. All helpers are safe to call before the tracker script has loaded — mutators are buffered, readers return a safe sentinel.

import {
  enableCookies,
  disableCookies,
  cookiesEnabled,
  getCookies,
  getVisitorId,
  ready,
} from "@hellyeah/x-ray";

// On consent granted:
enableCookies();

// On consent revoked:
disableCookies();

// Synchronous check (false until the tracker loads):
if (cookiesEnabled()) {
  /* … */
}

// Read the current cookie payload directly from hy_attr:
const attr = getCookies();

// Convenience helper:
const visitorId = getVisitorId();

// Await tracker script readiness:
await ready();

The helpers are re-exported from every framework entrypoint (@hellyeah/x-ray/{react,next,remix,vue,nuxt,svelte,astro}). For non-framework browser utilities, import them from @hellyeah/x-ray/browser.

HyAttrCookie shape

hy_attr is base64url JSON. getCookies() parses it directly, so it can return visitor attribution before the remote tracker runtime has finished loading. It returns HyAttrCookie | null:

type HyAttrCookie = {
  vid?: string;       // visitor ID
  ts?: number;        // first-seen timestamp
  gclid?: string;     // Google Ads
  gbraid?: string;    // Google Ads (web→app)
  wbraid?: string;    // Google Ads (app→web)
  fbclid?: string;    // Meta
  msclkid?: string;   // Microsoft Ads
  ttclid?: string;    // TikTok
  li_fat_id?: string; // LinkedIn
  twclid?: string;    // X (Twitter)
  utm_source?: string;
  utm_medium?: string;
  utm_campaign?: string;
  utm_content?: string;
  utm_term?: string;
};

Server-side identity

The server SDK splits identity into two orthogonal pieces:

identify({ distinctId }) — process-local default. It sets the distinctId used when track() is called without one. It does not send any wire item and does not accept PII.

xray.identify({ distinctId: "user_42" });
xray.track("signup"); // uses user_42 from above

Concurrency warning. In shared-singleton server contexts (one XRay instance serving many requests), identify() races across requests — whoever called it last wins for every subsequent track(). Always pass distinctId per-call in multi-user handlers.

track(name, { distinctId, identity, visitorId }) — per-call PII and visitor context. identity carries Enhanced Conversions email / phone; visitorId forwards a cookie-derived UUID so events are attributable to the visitor's session.

await xray.trackImmediate("cv_purchase", {
  distinctId: "user_42",
  eventId: stripeEvent.id,
  identity: { email: "jane@example.com", phone: "+15555551234" },
  visitorId: req.cookies.hy_vid,
  revenue: 49.99,
  currency: "USD",
});

Notes:

  • identity requires an explicit distinctId on the same call. The identify() default is not applied when identity is present — this prevents PII from being attached to the wrong user in concurrent-server environments.
  • identity: {} or empty/whitespace-only fields are treated as absent (no identify item emitted).
  • eventId maps to hy_event_id on the wire. Pass a stable webhook or job event ID to make retries idempotent.
  • visitor_id is session-scoped and COALESCE first-write-wins: once a value is recorded for a given distinctId, subsequent mixed visitorIds resolve to the original. This is backend design, not data loss.

Payload limits

Server metadata accepts flat primitive values only: strings, numbers, booleans, and null. The SDK sends at most 20 metadata properties, truncates keys to 100 characters, and truncates string values to 500 characters. Non-flat values are dropped. Use options.logger or onError if you need visibility into sanitization and delivery drops.

Attribution

Constructor-level context is applied to every event from the instance. In addition to UTMs, the SDK carries the following click IDs:

Key Platform
gclid Google Ads
gbraid Google Ads (web → app)
wbraid Google Ads (app → web)
fbclid Meta
msclkid Microsoft Ads
ttclid TikTok
li_fat_id LinkedIn
twclid X (Twitter)
const xray = new XRay("your-website-id", {
  context: {
    utm_source: "google",
    gclid: req.query.gclid,
    gbraid: req.query.gbraid,
    wbraid: req.query.wbraid,
  },
});

Because context is constructor-scoped, per-request click IDs require a per-request XRay instance. A shared singleton will apply the same click IDs to every event.

Error handling

The SDK is fire-and-forget by convention. User-facing surfaces never reject on HTTP failure:

Surface Contract
track() Returns void. Enqueues synchronously, never throws.
trackImmediate() Returns Promise<void>. Resolves even on HTTP failure (errors logged).
flush() Returns Promise<void>. Failed partitions are re-queued; does not reject.
shutdown() Returns Promise<void>. Races flush() against timeoutMs; does not reject.

Broken analytics must not break your request path. To observe drops (4xx malformed payloads, exhausted 5xx/network retries, queue overflow, identity misuse), opt in with the onError hook:

import { XRay, type XRayError } from "@hellyeah/x-ray/server";

const xray = new XRay("your-website-id", {
  onError: (err: XRayError) => {
    switch (err.type) {
      case "http_4xx":
        // Malformed payload — items dropped, will not retry.
        Sentry.captureMessage("xray 4xx", { extra: err });
        break;
      case "http_5xx_exhausted":
        // Server errors after all retries — items re-queued.
        Sentry.captureMessage("xray 5xx exhausted", { extra: err });
        break;
      case "network_exhausted":
        // Network failure after all retries — items re-queued.
        Sentry.captureException(err.cause, { extra: err });
        break;
      case "queue_overflow":
        // maxQueueSize exceeded — oldest item dropped.
        Sentry.captureMessage("xray queue overflow", { extra: err });
        break;
      case "identity_missing_distinct_id":
        // track() with identity but no distinctId — item dropped.
        Sentry.captureMessage(`xray dropped ${err.event}`, { extra: err });
        break;
    }
  },
});

The hook fires in addition to structured log entries (via options.logger). Throws from the hook are caught and logged — they will not crash the SDK.

License

MIT

Related Packages

create-contioreach-blog

Create a blog wired to ContioReach in one command. Pick Next.js, Nuxt, Astro, Sv...

479 0
@passlock/browser

Flexible passkey authentication for Astro, SvelteKit, NextJS and other framework...

5,281 192
@loomail/browser

Browser client for Loomail: identify people and track events from any web app, w...

410 0
consentlayer

Consent-first analytics for the browser and server: one API in front of GA4, Met...

0 1
gizmoanalytics-mcp

Cookieless web analytics for AI coding agents. Cursor / Claude / any MCP-aware t...

772

Version History

Version Released License
1.2.0 MIT
1.1.0 MIT
1.0.0 MIT
0.3.0 MIT
0.2.1 MIT
Pre-releases (3)
Version Released License
1.0.0-rc.1 MIT
0.1.0-beta.6 MIT
0.1.0-beta.5 MIT