@hellyeah/x-ray
v1.1.0
Published
Multi-platform analytics SDK for browser, React, Next.js, Remix, Vue, Svelte, Nuxt, Astro, and server environments
Readme

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
Install the package
npm install @hellyeah/x-rayAdd 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" mode="on" /> </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", mode: "on" }); 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: 1andflushInterval: 0so 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
trackImmediateto bypass the queue entirely and await delivery:await xray.trackImmediate("signup", { distinctId: "user_42" });Deploy your app and see data flowing in.
Environment tags and mode
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.
mode is the kill switch:
<Analytics websiteId="your-website-id" mode="off" />const xray = new XRay("your-website-id", { mode: "off" });When mode is "off", the browser SDK does not inject the tracker
script, does not mutate the DOM, does not write cookies, and public
methods become no-ops. The server SDK does not start a flush timer and
track(), trackImmediate(), identify(), and flush() are no-ops.
Resolution order:
| Surface | Option | Env var | Default |
| ------- | ------ | ------- | ------- |
| Next.js browser | env | NEXT_PUBLIC_HELLYEAH_TRACKER_ENV | "prod" |
| Next.js browser | mode | NEXT_PUBLIC_HELLYEAH_TRACKER_MODE | "on" |
| Server | env | HELLYEAH_TRACKER_ENV | "prod" |
| Server | mode | HELLYEAH_TRACKER_MODE | "on" |
NEXT_PUBLIC_* values are bundled at build time by Next.js. Changing
NEXT_PUBLIC_HELLYEAH_TRACKER_ENV or
NEXT_PUBLIC_HELLYEAH_TRACKER_MODE requires a rebuild and redeploy.
For Vite, Astro, Nuxt, and other browser builds, pass env and mode
from that framework's own env mechanism. For runtime control, pass the
prop explicitly.
disabled: true is deprecated and remains an alias for
mode: "off" for one major version. It logs:
[hellyeah] 'disabled' is deprecated, use mode: 'off' instead. Will be removed in v2.disabled: false is ignored. If both disabled and mode are passed,
mode wins.
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: "[email protected]", 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 aboveConcurrency warning. In shared-singleton server contexts (one
XRayinstance serving many requests),identify()races across requests — whoever called it last wins for every subsequenttrack(). Always passdistinctIdper-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: "[email protected]", phone: "+15555551234" },
visitorId: req.cookies.hy_vid,
revenue: 49.99,
currency: "USD",
});Notes:
identityrequires an explicitdistinctIdon the same call. Theidentify()default is not applied whenidentityis 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).eventIdmaps tohy_event_idon the wire. Pass a stable webhook or job event ID to make retries idempotent.visitor_idis session-scoped and COALESCE first-write-wins: once a value is recorded for a givendistinctId, subsequent mixedvisitorIds 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.
