A lightweight, modular StickyQR Analytics SDK for browser applications. Built as a modern alternative to Segment.js with TypeScript support, plugin architecture, and privacy-first design.
- Identity Management: Track users with
identify()andalias() - Event Tracking: Track custom events with
track() - Page Views: Automatic and manual page tracking with
page() - Screen Views: SPA screen tracking with
screen() - Group Tracking: Associate users with groups/organizations
- Plugin System: Extensible architecture with enrichment and destination plugins
- Queue & Retry: Automatic batching and retry logic for reliable delivery
- Privacy-First: localStorage/cookie fallback, configurable storage
- TypeScript: Full type safety and IntelliSense support
- Zero Dependencies: Lightweight bundle size
npm install @stickyqr/analyticsOr use via CDN:
<script src="https://cdn.stickyqr.com/analytics/1.0.0/index.umd.js"></script>import { Analytics } from "@stickyqr/analytics";
const analytics = new Analytics({
writeKey: "your-write-key",
debug: true,
});// Automatic page tracking (enabled by default)
// Or manually track pages
analytics.page("Home");
analytics.page("Pricing", "Marketing", { plan: "enterprise" });// Identify user
analytics.identify("user-123", {
email: "[email protected]",
name: "John Doe",
plan: "premium",
});
// Update traits for current user
analytics.identify(undefined, {
lastLogin: new Date().toISOString(),
});analytics.track("Button Clicked", {
buttonId: "cta-signup",
page: "homepage",
});
analytics.track("Purchase Completed", {
orderId: "order-123",
revenue: 99.99,
currency: "USD",
products: ["product-1", "product-2"],
});analytics.screen("Dashboard", "App", {
section: "overview",
});// Link anonymous user to identified user
analytics.alias("user-123", "anonymous-id-456");analytics.group("company-123", {
name: "Acme Inc",
plan: "enterprise",
employees: 50,
});const analytics = new Analytics({
// Required
writeKey: "your-write-key",
apiHost: "https://api.stickyqr.com/analytics", // Optional, defaults to https://api.stickyqr.com/analytics
flushAt: 20, // Flush after N events
flushInterval: 10000, // Flush every N ms
maxQueueSize: 100, // Max events in queue
retryAttempts: 3, // Retry failed requests
debug: false, // Enable debug logging
trackPageViews: true, // Auto-track page views
anonymousIdKey: "stickyqr_analytics_anonymous_id",
userIdKey: "stickyqr_analytics_user_id",
// Custom fetch function (optional)
// Useful for Next.js, custom headers, or testing
customFetch: (url, init) => fetch(url, { ...init, cache: "no-store" }),
// Plugins
plugins: [
new DeviceEnrichmentPlugin(),
new GoogleAnalyticsPlugin({ measurementId: "G-XXXXXXXXXX" }),
],
});The customFetch option allows you to provide a custom fetch function for making HTTP requests. This is useful for:
- Next.js App Router: Disable caching for analytics requests
- Custom headers: Add authentication or custom headers
- Proxying: Route requests through your own server
- Testing: Mock network requests in tests
const analytics = new Analytics({
writeKey: "your-write-key",
customFetch: (url, init) =>
fetch(url, {
...init,
cache: "no-store",
}),
});const analytics = new Analytics({
writeKey: "your-write-key",
customFetch: (url, init) =>
fetch(url, {
...init,
headers: {
...init?.headers,
"X-Custom-Header": "my-value",
"X-API-Version": "2024-01",
},
}),
});const analytics = new Analytics({
writeKey: "your-write-key",
apiHost: "/api/analytics-proxy", // Your proxy endpoint
customFetch: (url, init) => fetch(url, init),
});const mockFetch = jest.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ success: true }),
});
const analytics = new Analytics({
writeKey: "test-key",
customFetch: mockFetch,
});
// Your tests...
expect(mockFetch).toHaveBeenCalled();import { Analytics, ConsoleLoggerPlugin } from "@stickyqr/analytics";
const analytics = new Analytics({
writeKey: "your-write-key",
plugins: [new ConsoleLoggerPlugin()],
});import { DeviceEnrichmentPlugin } from "@stickyqr/analytics";
const analytics = new Analytics({
writeKey: "your-write-key",
plugins: [new DeviceEnrichmentPlugin()],
});
// Adds device, browser, OS info to all eventsimport { GoogleAnalyticsPlugin } from "@stickyqr/analytics";
const analytics = new Analytics({
writeKey: "your-write-key",
plugins: [
new GoogleAnalyticsPlugin({
measurementId: "G-XXXXXXXXXX",
}),
],
});Create your own plugins by implementing the Plugin interface:
import { Plugin, TrackEvent } from "@stickyqr/analytics";
class MyCustomPlugin implements Plugin {
name = "my-plugin";
type: "enrichment" = "enrichment";
version = "1.0.0";
private loaded = false;
isLoaded(): boolean {
return this.loaded;
}
async load(): Promise<void> {
// Initialize your plugin
this.loaded = true;
}
async track(event: TrackEvent): Promise<TrackEvent> {
// Modify or send the event
return {
...event,
properties: {
...event.properties,
customField: "value",
},
};
}
}
// Register plugin
analytics.register(new MyCustomPlugin());- before: Pre-processing before event creation
- enrichment: Enrich events with additional data
- destination: Send events to external services
- after: Post-processing after all other plugins
Associate user with their actions and traits.
analytics.identify("user-123", {
email: "[email protected]",
name: "John Doe",
});Track a custom event.
analytics.track("Video Played", {
videoId: "video-123",
duration: 120,
});Track a page view.
analytics.page("Pricing", "Marketing", {
experiment: "variant-a",
});Track a screen view (for SPAs).
analytics.screen("Settings", "App", {
section: "profile",
});Link user identities.
analytics.alias("user-123", "anonymous-456");Associate user with a group.
analytics.group("company-123", {
name: "Acme Inc",
plan: "enterprise",
});Get current user information.
const { userId, anonymousId, traits } = analytics.user();Reset user (logout).
analytics.reset();Manually flush the event queue.
await analytics.flush();Print debug information to console.
analytics.debug();All tracking methods accept an optional options parameter:
analytics.track(
"Event Name",
{ prop: "value" },
{
timestamp: new Date("2024-01-01"),
context: {
ip: "192.168.1.1",
userAgent: "custom-ua",
},
integrations: {
"Google Analytics": false, // Disable for specific integration
},
}
);// Don't track until user consent
let analytics;
function onUserConsent() {
analytics = new Analytics({
writeKey: "your-write-key",
});
}
// Clear all data on user request
analytics.reset();
localStorage.clear(); // Remove stored IDsimport { Analytics } from "@stickyqr/analytics";
import { createContext, useContext } from "react";
const AnalyticsContext = createContext<Analytics | null>(null);
export function AnalyticsProvider({ children }) {
const analytics = new Analytics({
writeKey: "your-write-key",
});
return (
<AnalyticsContext.Provider value={analytics}>
{children}
</AnalyticsContext.Provider>
);
}
export function useAnalytics() {
return useContext(AnalyticsContext);
}
// Usage
function MyComponent() {
const analytics = useAnalytics();
const handleClick = () => {
analytics.track("Button Clicked");
};
return <button onClick={handleClick}>Click me</button>;
}import { Analytics } from "@stickyqr/analytics";
const analytics = new Analytics({
writeKey: "your-write-key",
});
export default {
install(app) {
app.config.globalProperties.$analytics = analytics;
app.provide("analytics", analytics);
},
};
// main.js
import { createApp } from "vue";
import analyticsPlugin from "./analytics";
createApp(App).use(analyticsPlugin).mount("#app");
// Usage in component
export default {
methods: {
trackEvent() {
this.$analytics.track("Button Clicked");
},
},
};When using Next.js App Router, the global fetch is patched by Next.js for caching. Use customFetch to disable caching for analytics requests:
// lib/analytics.ts
import { Analytics } from "@stickyqr/analytics";
export const analytics = new Analytics({
writeKey: process.env.NEXT_PUBLIC_STICKYQR_WRITE_KEY!,
// Disable Next.js caching for analytics requests
customFetch: (url, init) =>
fetch(url, {
...init,
cache: "no-store",
}),
});Alternative with next.revalidate:
export const analytics = new Analytics({
writeKey: process.env.NEXT_PUBLIC_STICKYQR_WRITE_KEY!,
customFetch: (url, init) =>
fetch(url, {
...init,
next: { revalidate: 0 },
}),
});// lib/analytics.ts
import { Analytics } from "@stickyqr/analytics";
export const analytics = new Analytics({
writeKey: process.env.NEXT_PUBLIC_STICKYQR_WRITE_KEY!,
});
// _app.tsx
import { useEffect } from "react";
import { useRouter } from "next/router";
import { analytics } from "../lib/analytics";
function MyApp({ Component, pageProps }) {
const router = useRouter();
useEffect(() => {
// Track page views on route change
const handleRouteChange = (url) => {
analytics.page(url);
};
router.events.on("routeChangeComplete", handleRouteChange);
return () => {
router.events.off("routeChangeComplete", handleRouteChange);
};
}, [router.events]);
return <Component {...pageProps} />;
}For tracking in Server Components or Server Actions, always use customFetch:
// lib/analytics-server.ts
import { Analytics } from "@stickyqr/analytics";
export const serverAnalytics = new Analytics({
writeKey: process.env.ANALYTICS_WRITE_KEY!,
trackPageViews: false,
customFetch: (url, init) =>
fetch(url, {
...init,
cache: "no-store",
}),
});
// app/actions.ts
("use server");
import { serverAnalytics } from "@/lib/analytics-server";
export async function submitForm(formData: FormData) {
// Process form...
await serverAnalytics.track("Form Submitted", {
formId: "contact-form",
});
await serverAnalytics.flush();
}The SDK works seamlessly in Node.js environments for server-side tracking.
import { Analytics } from "@stickyqr/analytics";
const analytics = new Analytics({
writeKey: process.env.ANALYTICS_WRITE_KEY || "your-write-key",
debug: process.env.NODE_ENV === "development",
flushAt: 20, // Batch 20 events before sending
flushInterval: 10000, // Flush every 10 seconds
maxQueueSize: 1000, // Higher for server environments
trackPageViews: false, // Disable auto page tracking in Node
});import express from "express";
import { Analytics } from "@stickyqr/analytics";
const app = express();
const analytics = new Analytics({
writeKey: process.env.ANALYTICS_WRITE_KEY!,
trackPageViews: false,
});
// Track API requests
app.use((req, res, next) => {
const startTime = Date.now();
res.on("finish", () => {
analytics.track("API Request", {
method: req.method,
path: req.path,
statusCode: res.statusCode,
duration: Date.now() - startTime,
userAgent: req.headers["user-agent"],
ip: req.ip,
});
});
next();
});
// User registration
app.post("/api/register", async (req, res) => {
const { email, name } = req.body;
const user = await createUser({ email, name });
// Identify user
await analytics.identify(user.id, {
email: user.email,
name: user.name,
platform: "api",
});
// Track signup
await analytics.track("User Registered", {
userId: user.id,
source: req.headers.referer,
});
res.json({ success: true, userId: user.id });
});app.post("/api/orders", async (req, res) => {
const { userId, items, total } = req.body;
const order = await createOrder({ userId, items, total });
// Track purchase
await analytics.track("Order Completed", {
userId,
orderId: order.id,
revenue: order.total,
currency: "USD",
products: order.items.map((item) => ({
productId: item.id,
name: item.name,
price: item.price,
quantity: item.quantity,
})),
});
res.json({ success: true, orderId: order.id });
});import cron from "node-cron";
cron.schedule("0 0 * * *", async () => {
analytics.track("Daily Job Started", {
jobName: "cleanup",
timestamp: new Date(),
});
try {
await runDailyCleanup();
analytics.track("Daily Job Completed", {
jobName: "cleanup",
});
} catch (error) {
analytics.track("Daily Job Failed", {
jobName: "cleanup",
error: error.message,
});
}
// Flush events before job completes
await analytics.flush();
});app.post("/webhooks/stripe", async (req, res) => {
const event = req.body;
switch (event.type) {
case "payment_intent.succeeded":
analytics.track("Payment Succeeded", {
userId: event.data.object.customer,
amount: event.data.object.amount / 100,
currency: event.data.object.currency,
});
break;
case "customer.subscription.created":
analytics.track("Subscription Created", {
userId: event.data.object.customer,
plan: event.data.object.plan.id,
});
break;
}
res.json({ received: true });
});Always flush events on server shutdown:
// Handle graceful shutdown
process.on("SIGTERM", async () => {
console.log("SIGTERM received, flushing analytics...");
await analytics.flush();
process.exit(0);
});
process.on("SIGINT", async () => {
console.log("SIGINT received, flushing analytics...");
await analytics.flush();
process.exit(0);
});process.on("uncaughtException", (error) => {
analytics.track("Uncaught Exception", {
error: error.message,
stack: error.stack,
timestamp: new Date(),
});
analytics.flush().then(() => {
process.exit(1);
});
});
process.on("unhandledRejection", (reason, promise) => {
analytics.track("Unhandled Rejection", {
reason: String(reason),
timestamp: new Date(),
});
});- Singleton Pattern: Create one Analytics instance per application
- Environment Variables: Use env vars for configuration
- Higher Queue Limits: Increase
maxQueueSizefor server environments - Manual Flushing: Call
flush()in background jobs and on shutdown - Error Handling: Track errors and exceptions for monitoring
- Fire-and-Forget: Don't await
track()unless critical (events are queued) - Disable Auto Page Tracking: Set
trackPageViews: falsein Node.js
- Node.js 18+ (for native
fetchsupport) - For Node.js 16 or earlier, install a fetch polyfill:
npm install node-fetch
ANALYTICS_WRITE_KEY=your-write-key
NODE_ENV=production- Chrome/Edge 90+
- Firefox 88+
- Safari 14+
- iOS Safari 14+
- Android Chrome 90+
- Node.js 18+ (native
fetchsupport) - For Node.js 16 or earlier, install
node-fetch:npm install node-fetch
See NODEJS_ANALYSIS.md for detailed Node.js support analysis and recommendations.
MIT
Contributions are welcome! Please read our contributing guidelines.
For issues and questions:
- GitHub Issues: https://github.com/yourcompany/stickyqr-analytics-sdk/issues
- Email: [email protected]