@hellyeah/x-ray
v0.2.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" /> </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"); 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.
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() |
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
/>| 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. |
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,
} 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 (null until the tracker loads):
const attr = getCookies();The helpers are re-exported from every framework entrypoint
(@hellyeah/x-ray/{react,next,remix,vue,nuxt,svelte,astro}).
HyAttrCookie shape
getCookies() 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",
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).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.
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.
