From e965c169dbe94505fa2d6e27d0aa9a2da260b87d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Nov 2025 12:24:44 +0000 Subject: [PATCH 1/4] feat: add framework-specific libraries for React, Next.js, Vue, and Svelte Created separate library implementations for popular frontend frameworks: - React: Hooks-based library with ZoriProvider, useZori, usePageView, useTrackEvent, useIdentify, and TrackClick component - Next.js (client): App Router and Pages Router support with automatic route tracking using usePathname/useSearchParams - Next.js (server): Server-side tracking for Server Components, API Routes, Server Actions, and Middleware with cookie management - Vue.js: Vue 3 plugin with composables (useZori, usePageView, useTrackEvent, useIdentify) and Vue Router integration - Svelte: Svelte stores pattern with global store, trackClick action, and SvelteKit integration helpers Each library includes: - Full TypeScript support with type definitions - Comprehensive README with usage examples - Package.json configured for npm publishing - Framework-specific patterns and best practices - GDPR consent management - Auto page view tracking - User identification - Custom event tracking --- nextjs-server/README.md | 302 +++++++++++++++++++++++++++++++++ nextjs-server/index.ts | 287 ++++++++++++++++++++++++++++++++ nextjs-server/package.json | 34 ++++ nextjs/README.md | 220 ++++++++++++++++++++++++ nextjs/index.tsx | 282 +++++++++++++++++++++++++++++++ nextjs/package.json | 36 ++++ react/README.md | 203 ++++++++++++++++++++++ react/index.tsx | 274 ++++++++++++++++++++++++++++++ react/package.json | 33 ++++ svelte/README.md | 333 +++++++++++++++++++++++++++++++++++++ svelte/ZoriProvider.svelte | 14 ++ svelte/index.ts | 258 ++++++++++++++++++++++++++++ svelte/package.json | 34 ++++ vue/README.md | 274 ++++++++++++++++++++++++++++++ vue/index.ts | 313 ++++++++++++++++++++++++++++++++++ vue/package.json | 33 ++++ 16 files changed, 2930 insertions(+) create mode 100644 nextjs-server/README.md create mode 100644 nextjs-server/index.ts create mode 100644 nextjs-server/package.json create mode 100644 nextjs/README.md create mode 100644 nextjs/index.tsx create mode 100644 nextjs/package.json create mode 100644 react/README.md create mode 100644 react/index.tsx create mode 100644 react/package.json create mode 100644 svelte/README.md create mode 100644 svelte/ZoriProvider.svelte create mode 100644 svelte/index.ts create mode 100644 svelte/package.json create mode 100644 vue/README.md create mode 100644 vue/index.ts create mode 100644 vue/package.json diff --git a/nextjs-server/README.md b/nextjs-server/README.md new file mode 100644 index 0000000..9e5b266 --- /dev/null +++ b/nextjs-server/README.md @@ -0,0 +1,302 @@ +# @zorihq/nextjs-server + +Server-side tracking for Next.js with ZoriHQ Analytics. Use this in Server Components, API Routes, Server Actions, and Middleware. + +## Installation + +```bash +npm install @zorihq/nextjs-server +# or +pnpm add @zorihq/nextjs-server +# or +yarn add @zorihq/nextjs-server +``` + +## Usage + +### 1. Initialize the Client + +Create `lib/zori-server.ts`: + +```typescript +import { createZoriServer } from '@zorihq/nextjs-server'; + +export const zoriServer = createZoriServer({ + publishableKey: process.env.ZORI_PUBLISHABLE_KEY!, + baseUrl: 'https://ingestion.zorihq.com/ingest', // optional +}); +``` + +### 2. Track Events in Server Components + +```tsx +import { zoriServer } from '@/lib/zori-server'; + +export default async function ProductPage({ params }: { params: { id: string } }) { + // Track page view + await zoriServer.track({ + eventName: 'product_viewed', + properties: { + product_id: params.id, + page_type: 'product', + }, + }); + + return
Product {params.id}
; +} +``` + +### 3. Track Events in API Routes (App Router) + +```typescript +// app/api/purchase/route.ts +import { NextResponse } from 'next/server'; +import { zoriServer } from '@/lib/zori-server'; + +export async function POST(request: Request) { + const body = await request.json(); + + // Track purchase event + await zoriServer.track({ + eventName: 'purchase_completed', + properties: { + product_id: body.productId, + amount: body.amount, + }, + }); + + return NextResponse.json({ success: true }); +} +``` + +### 4. Track Events in API Routes (Pages Router) + +```typescript +// pages/api/purchase.ts +import type { NextApiRequest, NextApiResponse } from 'next'; +import { zoriServer } from '@/lib/zori-server'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method === 'POST') { + await zoriServer.track({ + eventName: 'purchase_completed', + properties: { + product_id: req.body.productId, + amount: req.body.amount, + }, + }); + + res.status(200).json({ success: true }); + } +} +``` + +### 5. Identify Users in Server Actions + +```typescript +// app/actions.ts +'use server'; + +import { zoriServer } from '@/lib/zori-server'; + +export async function loginUser(email: string, userId: string) { + // Identify user + await zoriServer.identify({ + userInfo: { + app_id: userId, + email: email, + fullname: 'John Doe', + }, + }); + + // Track login event + await zoriServer.track({ + eventName: 'user_login', + properties: { + method: 'email', + }, + }); + + return { success: true }; +} +``` + +### 6. Track in Middleware + +```typescript +// middleware.ts +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; +import { createZoriServer } from '@zorihq/nextjs-server'; + +const zori = createZoriServer({ + publishableKey: process.env.ZORI_PUBLISHABLE_KEY!, +}); + +export async function middleware(request: NextRequest) { + // Track middleware events (e.g., auth checks, redirects) + await zori.track({ + eventName: 'middleware_check', + properties: { + path: request.nextUrl.pathname, + }, + pageUrl: request.url, + }); + + return NextResponse.next(); +} + +export const config = { + matcher: '/dashboard/:path*', +}; +``` + +## API Reference + +### ZoriServer Class + +#### Constructor + +```typescript +const zori = new ZoriServer({ + publishableKey: 'your-key', // required + baseUrl: 'https://ingestion.zorihq.com/ingest', // optional +}); +``` + +#### Methods + +##### `track(options: TrackEventOptions): Promise` + +Track a custom event. + +```typescript +await zori.track({ + eventName: 'button_clicked', + properties: { + button_name: 'signup', + location: 'header', + }, + // Optional overrides: + visitorId: 'custom-visitor-id', + sessionId: 'custom-session-id', + userAgent: 'custom-user-agent', + pageUrl: 'https://example.com/page', + host: 'example.com', + referrer: 'https://google.com', +}); +``` + +##### `identify(options: IdentifyOptions): Promise` + +Identify a user. + +```typescript +await zori.identify({ + userInfo: { + app_id: 'user_123', + email: 'user@example.com', + fullname: 'John Doe', + plan: 'premium', // Custom properties + signup_date: '2025-01-15', + }, + // Optional overrides: + visitorId: 'custom-visitor-id', + sessionId: 'custom-session-id', + userAgent: 'custom-user-agent', + pageUrl: 'https://example.com/page', + host: 'example.com', +}); +``` + +##### `getVisitorId(): Promise` + +Get the current visitor ID from cookies. + +```typescript +const visitorId = await zori.getVisitorId(); +``` + +##### `getSessionId(): Promise` + +Get the current session ID from cookies. + +```typescript +const sessionId = await zori.getSessionId(); +``` + +##### `getOrCreateVisitorId(): Promise` + +Get or create a visitor ID (sets cookie if not exists). + +```typescript +const visitorId = await zori.getOrCreateVisitorId(); +``` + +##### `getOrCreateSessionId(): Promise` + +Get or create a session ID (sets cookie if not exists). + +```typescript +const sessionId = await zori.getOrCreateSessionId(); +``` + +## Features + +### Automatic Cookie Management + +The library automatically manages visitor and session cookies: + +- `zori_visitor_id` - 2 year expiry +- `zori_session_id` - Browser session expiry + +### Automatic Request Metadata + +Automatically extracts from Next.js headers: + +- User-Agent +- Referrer +- Host + +### UTM Parameter Tracking + +Automatically extracts UTM parameters from the `pageUrl` option. + +## Environment Variables + +Create `.env.local`: + +```env +ZORI_PUBLISHABLE_KEY=your-publishable-key +``` + +## Best Practices + +1. **Initialize once**: Create a single instance and reuse it across your app +2. **Async/await**: Always await tracking calls in critical paths +3. **Error handling**: Tracking failures won't throw errors but return `false` +4. **Middleware**: Keep middleware tracking lightweight to avoid latency + +## TypeScript Support + +This package includes full TypeScript definitions. + +```typescript +import type { + ZoriConfig, + TrackEventOptions, + IdentifyOptions, + UserInfo, +} from '@zorihq/nextjs-server'; +``` + +## Client-Side Tracking + +For client-side tracking (hooks, components, browser events), use `@zorihq/nextjs` instead. + +## License + +MIT diff --git a/nextjs-server/index.ts b/nextjs-server/index.ts new file mode 100644 index 0000000..d5a31fa --- /dev/null +++ b/nextjs-server/index.ts @@ -0,0 +1,287 @@ +import { cookies, headers } from 'next/headers'; + +const VERSION = '1.0.0'; +const COOKIE_NAME = 'zori_visitor_id'; +const SESSION_COOKIE_NAME = 'zori_session_id'; +const COOKIE_EXPIRY_DAYS = 365 * 2; // 2 years +const DEFAULT_API_URL = 'https://ingestion.zorihq.com/ingest'; + +// Types +export interface ZoriConfig { + publishableKey: string; + baseUrl?: string; +} + +export interface UserInfo { + app_id?: string; + email?: string; + fullname?: string; + full_name?: string; + [key: string]: any; +} + +export interface TrackEventOptions { + eventName: string; + properties?: Record; + visitorId?: string; + sessionId?: string; + userAgent?: string; + pageUrl?: string; + host?: string; + referrer?: string; +} + +export interface IdentifyOptions { + userInfo: UserInfo; + visitorId?: string; + sessionId?: string; + userAgent?: string; + pageUrl?: string; + host?: string; +} + +// Main Zori Server Class +export class ZoriServer { + private config: Required; + + constructor(config: ZoriConfig) { + this.config = { + publishableKey: config.publishableKey, + baseUrl: config.baseUrl || DEFAULT_API_URL, + }; + + if (!this.config.publishableKey) { + throw new Error('[ZoriHQ] publishableKey is required'); + } + } + + /** + * Generate a unique visitor ID + */ + private generateVisitorId(): string { + return ( + 'vis_' + + 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + const r = (Math.random() * 16) | 0; + const v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }) + ); + } + + /** + * Generate a unique session ID + */ + private generateSessionId(): string { + return ( + 'ses_' + + Date.now().toString(36) + + '_' + + Math.random().toString(36).substring(2, 9) + ); + } + + /** + * Generate a unique event ID + */ + private generateEventId(): string { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + const r = (Math.random() * 16) | 0; + const v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); + } + + /** + * Get or create visitor ID from cookies + */ + async getOrCreateVisitorId(): Promise { + const cookieStore = await cookies(); + let visitorId = cookieStore.get(COOKIE_NAME)?.value; + + if (!visitorId) { + visitorId = this.generateVisitorId(); + cookieStore.set(COOKIE_NAME, visitorId, { + maxAge: COOKIE_EXPIRY_DAYS * 24 * 60 * 60, + path: '/', + sameSite: 'lax', + }); + } + + return visitorId; + } + + /** + * Get or create session ID from cookies + */ + async getOrCreateSessionId(): Promise { + const cookieStore = await cookies(); + let sessionId = cookieStore.get(SESSION_COOKIE_NAME)?.value; + + if (!sessionId) { + sessionId = this.generateSessionId(); + cookieStore.set(SESSION_COOKIE_NAME, sessionId, { + path: '/', + sameSite: 'lax', + }); + } + + return sessionId; + } + + /** + * Get request metadata from Next.js headers + */ + private async getRequestMetadata() { + const headersList = await headers(); + return { + userAgent: headersList.get('user-agent') || 'unknown', + referrer: headersList.get('referer') || null, + host: headersList.get('host') || 'unknown', + }; + } + + /** + * Extract UTM parameters from URL + */ + private getUTMParameters(url: string): Record | null { + try { + const urlObj = new URL(url); + const utmParams: Record = {}; + + ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'].forEach((param) => { + const value = urlObj.searchParams.get(param); + if (value) { + utmParams[param] = value; + } + }); + + return Object.keys(utmParams).length > 0 ? utmParams : null; + } catch { + return null; + } + } + + /** + * Send event to ingestion endpoint + */ + private async sendEvent(eventData: any, endpoint: string = '/ingest'): Promise { + try { + const baseUrl = this.config.baseUrl.replace(/\/ingest$/, ''); + const url = baseUrl + endpoint; + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Zori-PT': this.config.publishableKey, + 'X-Zori-Version': VERSION, + }, + body: JSON.stringify(eventData), + }); + + if (!response.ok) { + console.warn(`[ZoriHQ] Failed to send event to ${endpoint}:`, response.status); + return false; + } + + return true; + } catch (error) { + console.error(`[ZoriHQ] Error sending event to ${endpoint}:`, error); + return false; + } + } + + /** + * Track a custom event + */ + async track(options: TrackEventOptions): Promise { + const metadata = await this.getRequestMetadata(); + const visitorId = options.visitorId || (await this.getOrCreateVisitorId()); + const sessionId = options.sessionId || (await this.getOrCreateSessionId()); + + const eventData = { + event_name: options.eventName, + client_generated_event_id: this.generateEventId(), + visitor_id: visitorId, + session_id: sessionId, + client_timestamp_utc: new Date().toISOString(), + user_agent: options.userAgent || metadata.userAgent, + referrer: options.referrer || metadata.referrer, + page_url: options.pageUrl || '/', + host: options.host || metadata.host, + utm_parameters: options.pageUrl ? this.getUTMParameters(options.pageUrl) : null, + }; + + if (options.properties && Object.keys(options.properties).length > 0) { + (eventData as any).custom_properties = options.properties; + } + + return await this.sendEvent(eventData); + } + + /** + * Identify a user + */ + async identify(options: IdentifyOptions): Promise { + const metadata = await this.getRequestMetadata(); + const visitorId = options.visitorId || (await this.getOrCreateVisitorId()); + const sessionId = options.sessionId || (await this.getOrCreateSessionId()); + + const identifyData: any = { + visitor_id: visitorId, + session_id: sessionId, + client_timestamp_utc: new Date().toISOString(), + user_agent: options.userAgent || metadata.userAgent, + page_url: options.pageUrl || '/', + host: options.host || metadata.host, + }; + + if (options.userInfo.app_id) { + identifyData.app_id = options.userInfo.app_id; + } + + if (options.userInfo.email) { + identifyData.email = options.userInfo.email; + } + + if (options.userInfo.fullname || options.userInfo.full_name) { + identifyData.fullname = options.userInfo.fullname || options.userInfo.full_name; + } + + const additionalProps = { ...options.userInfo }; + delete additionalProps.app_id; + delete additionalProps.email; + delete additionalProps.fullname; + delete additionalProps.full_name; + + if (Object.keys(additionalProps).length > 0) { + identifyData.additional_properties = additionalProps; + } + + return await this.sendEvent(identifyData, '/identify'); + } + + /** + * Get the current visitor ID + */ + async getVisitorId(): Promise { + const cookieStore = await cookies(); + return cookieStore.get(COOKIE_NAME)?.value || null; + } + + /** + * Get the current session ID + */ + async getSessionId(): Promise { + const cookieStore = await cookies(); + return cookieStore.get(SESSION_COOKIE_NAME)?.value || null; + } +} + +// Export a factory function for easier usage +export function createZoriServer(config: ZoriConfig): ZoriServer { + return new ZoriServer(config); +} + +export default ZoriServer; diff --git a/nextjs-server/package.json b/nextjs-server/package.json new file mode 100644 index 0000000..10f2240 --- /dev/null +++ b/nextjs-server/package.json @@ -0,0 +1,34 @@ +{ + "name": "@zorihq/nextjs-server", + "version": "1.0.0", + "description": "ZoriHQ Analytics for Next.js (Server-side)", + "main": "dist/index.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "echo 'Build with your preferred bundler (rollup, tsup, etc.)'", + "test": "echo 'Tests not yet configured'" + }, + "keywords": [ + "analytics", + "nextjs", + "next", + "server", + "tracking", + "zorihq" + ], + "author": "ZoriHQ", + "license": "MIT", + "peerDependencies": { + "next": ">=13.0.0" + }, + "devDependencies": { + "next": "^14.0.0", + "typescript": "^5.0.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/zorihq/script.git", + "directory": "nextjs-server" + } +} diff --git a/nextjs/README.md b/nextjs/README.md new file mode 100644 index 0000000..de45071 --- /dev/null +++ b/nextjs/README.md @@ -0,0 +1,220 @@ +# @zorihq/nextjs + +Next.js client-side hooks and components for ZoriHQ Analytics. + +## Installation + +```bash +npm install @zorihq/nextjs +# or +pnpm add @zorihq/nextjs +# or +yarn add @zorihq/nextjs +``` + +## Usage with App Router (Next.js 13+) + +### 1. Create a Client Component Wrapper + +Create `app/providers.tsx`: + +```tsx +'use client'; + +import { ZoriProvider } from '@zorihq/nextjs'; + +export function Providers({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} +``` + +### 2. Wrap Your Root Layout + +Update `app/layout.tsx`: + +```tsx +import { Providers } from './providers'; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {children} + + + ); +} +``` + +### 3. Use in Client Components + +```tsx +'use client'; + +import { useZori } from '@zorihq/nextjs'; + +export default function MyPage() { + const { track, identify } = useZori(); + + const handlePurchase = async () => { + await track('purchase_completed', { + product_id: 'prod_123', + amount: 99.99, + }); + }; + + return ( + + ); +} +``` + +## Usage with Pages Router (Next.js 12) + +### 1. Wrap Your App + +Update `pages/_app.tsx`: + +```tsx +import { ZoriProvider } from '@zorihq/nextjs'; +import type { AppProps } from 'next/app'; + +export default function App({ Component, pageProps }: AppProps) { + return ( + + + + ); +} +``` + +## Features + +### Auto Route Tracking + +The `ZoriProvider` automatically tracks route changes in both App Router and Pages Router when `autoTrackPageViews={true}` is set. + +### Track Events + +```tsx +'use client'; + +import { useZori } from '@zorihq/nextjs'; + +function MyComponent() { + const { track } = useZori(); + + const handleClick = () => { + track('button_clicked', { button_name: 'signup' }); + }; + + return ; +} +``` + +### Identify Users + +```tsx +'use client'; + +import { useIdentify } from '@zorihq/nextjs'; + +function UserProfile({ user }) { + useIdentify(user ? { + app_id: user.id, + email: user.email, + fullname: user.name, + } : null); + + return
{user?.name}
; +} +``` + +### TrackClick Component + +```tsx +'use client'; + +import { TrackClick } from '@zorihq/nextjs'; + +function MyButton() { + return ( + + Get Started + + ); +} +``` + +## Environment Variables + +Create `.env.local`: + +```env +NEXT_PUBLIC_ZORI_KEY=your-publishable-key +``` + +## API Reference + +### ZoriProvider Props + +- `config` (required): Configuration object + - `publishableKey` (required): Your ZoriHQ publishable key + - `baseUrl` (optional): Custom ingestion endpoint + - `comebackThreshold` (optional): Minimum time away to trigger comeback event (ms) + - `trackQuickSwitches` (optional): Track all visibility changes +- `children` (required): Your app components +- `autoTrackPageViews` (optional): Auto-track page views on route change, default `true` + +### useZori Hook + +Returns an object with: + +- `isInitialized`: Boolean indicating if ZoriHQ is ready +- `track(eventName, properties)`: Track custom events +- `identify(userInfo)`: Identify users +- `getVisitorId()`: Get the visitor ID +- `getSessionId()`: Get the current session ID +- `setConsent(preferences)`: Set GDPR consent preferences +- `optOut()`: Opt out of tracking completely +- `hasConsent()`: Check if user has given consent + +## TypeScript Support + +This package includes TypeScript definitions out of the box. + +## Server-Side Tracking + +For server-side tracking (Server Components, API Routes, Middleware), use `@zorihq/nextjs-server` instead. + +## License + +MIT diff --git a/nextjs/index.tsx b/nextjs/index.tsx new file mode 100644 index 0000000..faf7b59 --- /dev/null +++ b/nextjs/index.tsx @@ -0,0 +1,282 @@ +'use client'; + +import React, { createContext, useContext, useEffect, useRef, useState, useCallback } from 'react'; +import { usePathname, useSearchParams } from 'next/navigation'; + +// Types +export interface ZoriConfig { + publishableKey: string; + baseUrl?: string; + comebackThreshold?: number; + trackQuickSwitches?: boolean; +} + +export interface ConsentPreferences { + analytics?: boolean; + marketing?: boolean; +} + +export interface UserInfo { + app_id?: string; + email?: string; + fullname?: string; + full_name?: string; + [key: string]: any; +} + +export interface ZoriContextType { + isInitialized: boolean; + track: (eventName: string, properties?: Record) => Promise; + identify: (userInfo: UserInfo) => Promise; + getVisitorId: () => Promise; + getSessionId: () => string | null; + setConsent: (preferences: ConsentPreferences) => boolean; + optOut: () => boolean; + hasConsent: () => boolean; +} + +// Context +const ZoriContext = createContext(null); + +// Provider Props +export interface ZoriProviderProps { + config: ZoriConfig; + children: React.ReactNode; + autoTrackPageViews?: boolean; +} + +// Provider Component +export const ZoriProvider: React.FC = ({ + config, + children, + autoTrackPageViews = true, +}) => { + const [isInitialized, setIsInitialized] = useState(false); + const scriptLoadedRef = useRef(false); + const pathname = usePathname(); + const searchParams = useSearchParams(); + + useEffect(() => { + if (scriptLoadedRef.current) return; + + // Initialize queue + (window as any).ZoriHQ = (window as any).ZoriHQ || []; + + // Load script + const script = document.createElement('script'); + script.src = 'https://cdn.zorihq.com/script.min.js'; + script.async = true; + script.setAttribute('data-key', config.publishableKey); + + if (config.baseUrl) { + script.setAttribute('data-base-url', config.baseUrl); + } + + if (config.comebackThreshold !== undefined) { + script.setAttribute('data-comeback-threshold', config.comebackThreshold.toString()); + } + + if (config.trackQuickSwitches !== undefined) { + script.setAttribute('data-track-quick-switches', config.trackQuickSwitches.toString()); + } + + script.onload = () => { + setIsInitialized(true); + }; + + document.head.appendChild(script); + scriptLoadedRef.current = true; + + return () => { + if (script.parentNode) { + script.parentNode.removeChild(script); + } + }; + }, [config]); + + // Auto-track page views on route change (App Router) + useEffect(() => { + if (isInitialized && autoTrackPageViews) { + const zori = (window as any).ZoriHQ; + if (zori) { + const properties = { + page_title: document.title, + page_path: pathname, + page_search: searchParams?.toString() || '', + page_hash: window.location.hash, + }; + + if (typeof zori.track === 'function') { + zori.track('page_view', properties); + } else { + zori.push(['track', 'page_view', properties]); + } + } + } + }, [pathname, searchParams, isInitialized, autoTrackPageViews]); + + const track = useCallback(async (eventName: string, properties?: Record) => { + const zori = (window as any).ZoriHQ; + if (!zori) return false; + + if (typeof zori.track === 'function') { + return await zori.track(eventName, properties); + } else { + zori.push(['track', eventName, properties]); + return true; + } + }, []); + + const identify = useCallback(async (userInfo: UserInfo) => { + const zori = (window as any).ZoriHQ; + if (!zori) return false; + + if (typeof zori.identify === 'function') { + return await zori.identify(userInfo); + } else { + zori.push(['identify', userInfo]); + return true; + } + }, []); + + const getVisitorId = useCallback(async () => { + const zori = (window as any).ZoriHQ; + if (!zori) return ''; + + if (typeof zori.getVisitorId === 'function') { + return await zori.getVisitorId(); + } + + return new Promise((resolve) => { + zori.push(['getVisitorId', (id: string) => resolve(id)]); + }); + }, []); + + const getSessionId = useCallback(() => { + const zori = (window as any).ZoriHQ; + if (!zori || typeof zori.getSessionId !== 'function') return null; + return zori.getSessionId(); + }, []); + + const setConsent = useCallback((preferences: ConsentPreferences) => { + const zori = (window as any).ZoriHQ; + if (!zori) return false; + + if (typeof zori.setConsent === 'function') { + return zori.setConsent(preferences); + } else { + zori.push(['setConsent', preferences]); + return true; + } + }, []); + + const optOut = useCallback(() => { + const zori = (window as any).ZoriHQ; + if (!zori) return false; + + if (typeof zori.optOut === 'function') { + return zori.optOut(); + } else { + zori.push(['optOut']); + return true; + } + }, []); + + const hasConsent = useCallback(() => { + const zori = (window as any).ZoriHQ; + if (!zori || typeof zori.hasConsent !== 'function') return true; + return zori.hasConsent(); + }, []); + + const contextValue: ZoriContextType = { + isInitialized, + track, + identify, + getVisitorId, + getSessionId, + setConsent, + optOut, + hasConsent, + }; + + return {children}; +}; + +// Hook to use Zori +export const useZori = (): ZoriContextType => { + const context = useContext(ZoriContext); + if (!context) { + throw new Error('useZori must be used within a ZoriProvider'); + } + return context; +}; + +// Hook to track events with dependencies +export const useTrackEvent = ( + eventName: string, + properties?: Record, + dependencies: any[] = [] +) => { + const { track, isInitialized } = useZori(); + + useEffect(() => { + if (isInitialized) { + track(eventName, properties); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isInitialized, ...dependencies]); +}; + +// Hook to identify user +export const useIdentify = (userInfo: UserInfo | null) => { + const { identify, isInitialized } = useZori(); + + useEffect(() => { + if (isInitialized && userInfo) { + identify(userInfo); + } + }, [isInitialized, userInfo, identify]); +}; + +// Component to track clicks +export interface TrackClickProps { + eventName?: string; + properties?: Record; + children: React.ReactNode; + as?: React.ElementType; + [key: string]: any; +} + +export const TrackClick: React.FC = ({ + eventName = 'click', + properties = {}, + children, + as: Component = 'button', + ...props +}) => { + const { track } = useZori(); + + const handleClick = useCallback( + (event: React.MouseEvent) => { + track(eventName, properties); + if (props.onClick) { + props.onClick(event); + } + }, + [track, eventName, properties, props] + ); + + return ( + + {children} + + ); +}; + +export default { + ZoriProvider, + useZori, + useTrackEvent, + useIdentify, + TrackClick, +}; diff --git a/nextjs/package.json b/nextjs/package.json new file mode 100644 index 0000000..ee19276 --- /dev/null +++ b/nextjs/package.json @@ -0,0 +1,36 @@ +{ + "name": "@zorihq/nextjs", + "version": "1.0.0", + "description": "ZoriHQ Analytics for Next.js (Client-side)", + "main": "dist/index.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "echo 'Build with your preferred bundler (rollup, tsup, etc.)'", + "test": "echo 'Tests not yet configured'" + }, + "keywords": [ + "analytics", + "nextjs", + "next", + "tracking", + "zorihq" + ], + "author": "ZoriHQ", + "license": "MIT", + "peerDependencies": { + "next": ">=13.0.0", + "react": ">=18.0.0" + }, + "devDependencies": { + "@types/react": "^18.0.0", + "next": "^14.0.0", + "react": "^18.0.0", + "typescript": "^5.0.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/zorihq/script.git", + "directory": "nextjs" + } +} diff --git a/react/README.md b/react/README.md new file mode 100644 index 0000000..3148aff --- /dev/null +++ b/react/README.md @@ -0,0 +1,203 @@ +# @zorihq/react + +React hooks and components for ZoriHQ Analytics. + +## Installation + +```bash +npm install @zorihq/react +# or +pnpm add @zorihq/react +# or +yarn add @zorihq/react +``` + +## Usage + +### 1. Wrap your app with ZoriProvider + +```tsx +import { ZoriProvider } from '@zorihq/react'; + +function App() { + return ( + + + + ); +} +``` + +### 2. Use the hooks + +#### useZori - Main Hook + +```tsx +import { useZori } from '@zorihq/react'; + +function MyComponent() { + const { track, identify, getVisitorId, setConsent, optOut } = useZori(); + + const handlePurchase = async () => { + await track('purchase_completed', { + product_id: 'prod_123', + amount: 99.99, + }); + }; + + const handleLogin = async () => { + await identify({ + app_id: 'user_123', + email: 'user@example.com', + fullname: 'John Doe', + }); + }; + + return ( +
+ + +
+ ); +} +``` + +#### usePageView - Auto Track Page Views + +```tsx +import { usePageView } from '@zorihq/react'; + +function ProductPage({ productId }) { + usePageView({ + product_id: productId, + page_type: 'product', + }); + + return
Product {productId}
; +} +``` + +#### useIdentify - Auto Identify Users + +```tsx +import { useIdentify } from '@zorihq/react'; + +function UserProfile({ user }) { + useIdentify(user ? { + app_id: user.id, + email: user.email, + fullname: user.name, + plan: user.subscription + } : null); + + return
{user?.name}
; +} +``` + +#### useTrackEvent - Track Events with Dependencies + +```tsx +import { useTrackEvent } from '@zorihq/react'; + +function SearchResults({ query, results }) { + useTrackEvent( + 'search_completed', + { + query, + result_count: results.length, + }, + [query, results.length] // Re-track when these change + ); + + return
{results.length} results for "{query}"
; +} +``` + +### 3. TrackClick Component + +Automatically track clicks on any element: + +```tsx +import { TrackClick } from '@zorihq/react'; + +function MyButton() { + return ( + + Sign Up + + ); +} + +// Works with any element +function MyLink() { + return ( + + Learn More + + ); +} +``` + +## API Reference + +### ZoriProvider Props + +- `config` (required): Configuration object + - `publishableKey` (required): Your ZoriHQ publishable key + - `baseUrl` (optional): Custom ingestion endpoint + - `comebackThreshold` (optional): Minimum time away to trigger comeback event (ms) + - `trackQuickSwitches` (optional): Track all visibility changes +- `children` (required): Your app components +- `autoTrackPageViews` (optional): Auto-track page views on mount, default `true` + +### useZori Hook + +Returns an object with: + +- `isInitialized`: Boolean indicating if ZoriHQ is ready +- `track(eventName, properties)`: Track custom events +- `identify(userInfo)`: Identify users +- `getVisitorId()`: Get the visitor ID +- `getSessionId()`: Get the current session ID +- `setConsent(preferences)`: Set GDPR consent preferences +- `optOut()`: Opt out of tracking completely +- `hasConsent()`: Check if user has given consent + +## TypeScript Support + +This package includes TypeScript definitions out of the box. + +```tsx +import { ZoriConfig, UserInfo, ConsentPreferences } from '@zorihq/react'; + +const config: ZoriConfig = { + publishableKey: 'your-key', +}; + +const user: UserInfo = { + app_id: 'user_123', + email: 'user@example.com', +}; +``` + +## License + +MIT diff --git a/react/index.tsx b/react/index.tsx new file mode 100644 index 0000000..6f2bf1e --- /dev/null +++ b/react/index.tsx @@ -0,0 +1,274 @@ +import React, { createContext, useContext, useEffect, useRef, useState, useCallback } from 'react'; + +// Types +export interface ZoriConfig { + publishableKey: string; + baseUrl?: string; + comebackThreshold?: number; + trackQuickSwitches?: boolean; +} + +export interface ConsentPreferences { + analytics?: boolean; + marketing?: boolean; +} + +export interface UserInfo { + app_id?: string; + email?: string; + fullname?: string; + full_name?: string; + [key: string]: any; +} + +export interface ZoriContextType { + isInitialized: boolean; + track: (eventName: string, properties?: Record) => Promise; + identify: (userInfo: UserInfo) => Promise; + getVisitorId: () => Promise; + getSessionId: () => string | null; + setConsent: (preferences: ConsentPreferences) => boolean; + optOut: () => boolean; + hasConsent: () => boolean; +} + +// Context +const ZoriContext = createContext(null); + +// Provider Props +export interface ZoriProviderProps { + config: ZoriConfig; + children: React.ReactNode; + autoTrackPageViews?: boolean; +} + +// Provider Component +export const ZoriProvider: React.FC = ({ + config, + children, + autoTrackPageViews = true, +}) => { + const [isInitialized, setIsInitialized] = useState(false); + const scriptLoadedRef = useRef(false); + + useEffect(() => { + if (scriptLoadedRef.current) return; + + // Initialize queue + (window as any).ZoriHQ = (window as any).ZoriHQ || []; + + // Load script + const script = document.createElement('script'); + script.src = 'https://cdn.zorihq.com/script.min.js'; + script.async = true; + script.setAttribute('data-key', config.publishableKey); + + if (config.baseUrl) { + script.setAttribute('data-base-url', config.baseUrl); + } + + if (config.comebackThreshold !== undefined) { + script.setAttribute('data-comeback-threshold', config.comebackThreshold.toString()); + } + + if (config.trackQuickSwitches !== undefined) { + script.setAttribute('data-track-quick-switches', config.trackQuickSwitches.toString()); + } + + script.onload = () => { + setIsInitialized(true); + }; + + document.head.appendChild(script); + scriptLoadedRef.current = true; + + return () => { + if (script.parentNode) { + script.parentNode.removeChild(script); + } + }; + }, [config]); + + const track = useCallback(async (eventName: string, properties?: Record) => { + const zori = (window as any).ZoriHQ; + if (!zori) return false; + + if (typeof zori.track === 'function') { + return await zori.track(eventName, properties); + } else { + zori.push(['track', eventName, properties]); + return true; + } + }, []); + + const identify = useCallback(async (userInfo: UserInfo) => { + const zori = (window as any).ZoriHQ; + if (!zori) return false; + + if (typeof zori.identify === 'function') { + return await zori.identify(userInfo); + } else { + zori.push(['identify', userInfo]); + return true; + } + }, []); + + const getVisitorId = useCallback(async () => { + const zori = (window as any).ZoriHQ; + if (!zori) return ''; + + if (typeof zori.getVisitorId === 'function') { + return await zori.getVisitorId(); + } + + return new Promise((resolve) => { + zori.push(['getVisitorId', (id: string) => resolve(id)]); + }); + }, []); + + const getSessionId = useCallback(() => { + const zori = (window as any).ZoriHQ; + if (!zori || typeof zori.getSessionId !== 'function') return null; + return zori.getSessionId(); + }, []); + + const setConsent = useCallback((preferences: ConsentPreferences) => { + const zori = (window as any).ZoriHQ; + if (!zori) return false; + + if (typeof zori.setConsent === 'function') { + return zori.setConsent(preferences); + } else { + zori.push(['setConsent', preferences]); + return true; + } + }, []); + + const optOut = useCallback(() => { + const zori = (window as any).ZoriHQ; + if (!zori) return false; + + if (typeof zori.optOut === 'function') { + return zori.optOut(); + } else { + zori.push(['optOut']); + return true; + } + }, []); + + const hasConsent = useCallback(() => { + const zori = (window as any).ZoriHQ; + if (!zori || typeof zori.hasConsent !== 'function') return true; + return zori.hasConsent(); + }, []); + + const contextValue: ZoriContextType = { + isInitialized, + track, + identify, + getVisitorId, + getSessionId, + setConsent, + optOut, + hasConsent, + }; + + return {children}; +}; + +// Hook to use Zori +export const useZori = (): ZoriContextType => { + const context = useContext(ZoriContext); + if (!context) { + throw new Error('useZori must be used within a ZoriProvider'); + } + return context; +}; + +// Hook to track page views +export const usePageView = (properties?: Record) => { + const { track, isInitialized } = useZori(); + + useEffect(() => { + if (isInitialized) { + track('page_view', { + page_title: document.title, + page_path: window.location.pathname, + page_search: window.location.search, + page_hash: window.location.hash, + ...properties, + }); + } + }, [isInitialized, track, properties]); +}; + +// Hook to track events with dependencies +export const useTrackEvent = ( + eventName: string, + properties?: Record, + dependencies: any[] = [] +) => { + const { track, isInitialized } = useZori(); + + useEffect(() => { + if (isInitialized) { + track(eventName, properties); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isInitialized, ...dependencies]); +}; + +// Hook to identify user +export const useIdentify = (userInfo: UserInfo | null) => { + const { identify, isInitialized } = useZori(); + + useEffect(() => { + if (isInitialized && userInfo) { + identify(userInfo); + } + }, [isInitialized, userInfo, identify]); +}; + +// Component to track clicks +export interface TrackClickProps { + eventName?: string; + properties?: Record; + children: React.ReactNode; + as?: React.ElementType; + [key: string]: any; +} + +export const TrackClick: React.FC = ({ + eventName = 'click', + properties = {}, + children, + as: Component = 'button', + ...props +}) => { + const { track } = useZori(); + + const handleClick = useCallback( + (event: React.MouseEvent) => { + track(eventName, properties); + if (props.onClick) { + props.onClick(event); + } + }, + [track, eventName, properties, props] + ); + + return ( + + {children} + + ); +}; + +export default { + ZoriProvider, + useZori, + usePageView, + useTrackEvent, + useIdentify, + TrackClick, +}; diff --git a/react/package.json b/react/package.json new file mode 100644 index 0000000..0244755 --- /dev/null +++ b/react/package.json @@ -0,0 +1,33 @@ +{ + "name": "@zorihq/react", + "version": "1.0.0", + "description": "ZoriHQ Analytics for React", + "main": "dist/index.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "echo 'Build with your preferred bundler (rollup, tsup, etc.)'", + "test": "echo 'Tests not yet configured'" + }, + "keywords": [ + "analytics", + "react", + "tracking", + "zorihq" + ], + "author": "ZoriHQ", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + }, + "devDependencies": { + "@types/react": "^18.0.0", + "react": "^18.0.0", + "typescript": "^5.0.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/zorihq/script.git", + "directory": "react" + } +} diff --git a/svelte/README.md b/svelte/README.md new file mode 100644 index 0000000..c1f0510 --- /dev/null +++ b/svelte/README.md @@ -0,0 +1,333 @@ +# @zorihq/svelte + +Svelte stores, actions, and components for ZoriHQ Analytics. + +## Installation + +```bash +npm install @zorihq/svelte +# or +pnpm add @zorihq/svelte +# or +yarn add @zorihq/svelte +``` + +## Usage + +### 1. Initialize Zori + +#### Option A: Global Store (Recommended) + +Initialize once in your root component or layout: + +```svelte + + + + +``` + +#### Option B: Component Context + +Use the `ZoriProvider` component: + +```svelte + + + + + + +``` + +### 2. Track Events + +```svelte + + + + +``` + +### 3. Use Actions + +#### trackClick Action + +Automatically track clicks on any element: + +```svelte + + + + + + View Pricing + +``` + +### 4. Use Helpers + +#### usePageView + +Auto-track page views on component mount: + +```svelte + + +
Product {productId}
+``` + +#### useTrackEvent + +Track events on component mount: + +```svelte + + +
User Profile
+``` + +#### useIdentify + +Identify users on component mount: + +```svelte + + +
{user?.name}
+``` + +### 5. Reactive Tracking + +```svelte + + + +
{results.length} results
+``` + +### 6. Check Initialization Status + +```svelte + + +{#if $zori.isInitialized} +

Analytics Ready!

+{:else} +

Loading analytics...

+{/if} +``` + +## SvelteKit Integration + +### +layout.svelte + +```svelte + + + +``` + +### Environment Variables + +Create `.env`: + +```env +VITE_ZORI_KEY=your-publishable-key +``` + +## API Reference + +### createZoriStore(config) + +Create a new Zori store instance. + +```typescript +import { createZoriStore } from '@zorihq/svelte'; + +const zori = createZoriStore({ + publishableKey: 'your-key', + baseUrl: 'https://ingestion.zorihq.com/ingest', // optional + comebackThreshold: 30000, // optional + trackQuickSwitches: false, // optional +}); +``` + +### initZori(config) + +Initialize global Zori store (recommended for app-wide usage). + +```typescript +import { initZori } from '@zorihq/svelte'; + +const zori = initZori({ publishableKey: 'your-key' }); +``` + +### getZori() + +Get the global Zori store instance. + +```typescript +import { getZori } from '@zorihq/svelte'; + +const zori = getZori(); +``` + +### ZoriStore + +The store exposes: + +- `isInitialized`: Readable store indicating if ZoriHQ is ready +- `track(eventName, properties)`: Track custom events +- `identify(userInfo)`: Identify users +- `getVisitorId()`: Get the visitor ID +- `getSessionId()`: Get the current session ID +- `setConsent(preferences)`: Set GDPR consent preferences +- `optOut()`: Opt out of tracking completely +- `hasConsent()`: Check if user has given consent + +### Actions + +#### trackClick + +```svelte + +``` + +### Helpers + +- `usePageView(properties?)`: Track page view on mount +- `useTrackEvent(eventName, properties?)`: Track event on mount +- `useIdentify(userInfo)`: Identify user on mount + +## TypeScript Support + +This package includes full TypeScript support: + +```typescript +import type { + ZoriConfig, + ZoriStore, + ConsentPreferences, + UserInfo, +} from '@zorihq/svelte'; +``` + +## Stores Pattern + +This library follows Svelte's stores pattern: + +- Use `$` to auto-subscribe in components +- Stores are reactive and update automatically +- Automatic cleanup on component destroy + +## License + +MIT diff --git a/svelte/ZoriProvider.svelte b/svelte/ZoriProvider.svelte new file mode 100644 index 0000000..f5daad8 --- /dev/null +++ b/svelte/ZoriProvider.svelte @@ -0,0 +1,14 @@ + + + diff --git a/svelte/index.ts b/svelte/index.ts new file mode 100644 index 0000000..4ebb3e5 --- /dev/null +++ b/svelte/index.ts @@ -0,0 +1,258 @@ +import { writable, readonly, derived, get, type Readable, type Writable } from 'svelte/store'; +import { onMount, onDestroy } from 'svelte'; + +// Types +export interface ZoriConfig { + publishableKey: string; + baseUrl?: string; + comebackThreshold?: number; + trackQuickSwitches?: boolean; +} + +export interface ConsentPreferences { + analytics?: boolean; + marketing?: boolean; +} + +export interface UserInfo { + app_id?: string; + email?: string; + fullname?: string; + full_name?: string; + [key: string]: any; +} + +export interface ZoriStore { + isInitialized: Readable; + track: (eventName: string, properties?: Record) => Promise; + identify: (userInfo: UserInfo) => Promise; + getVisitorId: () => Promise; + getSessionId: () => string | null; + setConsent: (preferences: ConsentPreferences) => boolean; + optOut: () => boolean; + hasConsent: () => boolean; +} + +// Create the Zori store +export function createZoriStore(config: ZoriConfig): ZoriStore { + const isInitialized = writable(false); + let scriptLoaded = false; + + const loadScript = () => { + if (scriptLoaded || typeof window === 'undefined') return; + + // Initialize queue + (window as any).ZoriHQ = (window as any).ZoriHQ || []; + + // Load script + const script = document.createElement('script'); + script.src = 'https://cdn.zorihq.com/script.min.js'; + script.async = true; + script.setAttribute('data-key', config.publishableKey); + + if (config.baseUrl) { + script.setAttribute('data-base-url', config.baseUrl); + } + + if (config.comebackThreshold !== undefined) { + script.setAttribute('data-comeback-threshold', config.comebackThreshold.toString()); + } + + if (config.trackQuickSwitches !== undefined) { + script.setAttribute('data-track-quick-switches', config.trackQuickSwitches.toString()); + } + + script.onload = () => { + isInitialized.set(true); + }; + + document.head.appendChild(script); + scriptLoaded = true; + }; + + const track = async (eventName: string, properties?: Record): Promise => { + const zori = (window as any).ZoriHQ; + if (!zori) return false; + + if (typeof zori.track === 'function') { + return await zori.track(eventName, properties); + } else { + zori.push(['track', eventName, properties]); + return true; + } + }; + + const identify = async (userInfo: UserInfo): Promise => { + const zori = (window as any).ZoriHQ; + if (!zori) return false; + + if (typeof zori.identify === 'function') { + return await zori.identify(userInfo); + } else { + zori.push(['identify', userInfo]); + return true; + } + }; + + const getVisitorId = async (): Promise => { + const zori = (window as any).ZoriHQ; + if (!zori) return ''; + + if (typeof zori.getVisitorId === 'function') { + return await zori.getVisitorId(); + } + + return new Promise((resolve) => { + zori.push(['getVisitorId', (id: string) => resolve(id)]); + }); + }; + + const getSessionId = (): string | null => { + const zori = (window as any).ZoriHQ; + if (!zori || typeof zori.getSessionId !== 'function') return null; + return zori.getSessionId(); + }; + + const setConsent = (preferences: ConsentPreferences): boolean => { + const zori = (window as any).ZoriHQ; + if (!zori) return false; + + if (typeof zori.setConsent === 'function') { + return zori.setConsent(preferences); + } else { + zori.push(['setConsent', preferences]); + return true; + } + }; + + const optOut = (): boolean => { + const zori = (window as any).ZoriHQ; + if (!zori) return false; + + if (typeof zori.optOut === 'function') { + return zori.optOut(); + } else { + zori.push(['optOut']); + return true; + } + }; + + const hasConsent = (): boolean => { + const zori = (window as any).ZoriHQ; + if (!zori || typeof zori.hasConsent !== 'function') return true; + return zori.hasConsent(); + }; + + // Load script immediately + if (typeof window !== 'undefined') { + loadScript(); + } + + return { + isInitialized: readonly(isInitialized), + track, + identify, + getVisitorId, + getSessionId, + setConsent, + optOut, + hasConsent, + }; +} + +// Global store instance (optional, for app-wide usage) +let globalStore: ZoriStore | null = null; + +export function initZori(config: ZoriConfig): ZoriStore { + if (!globalStore) { + globalStore = createZoriStore(config); + } + return globalStore; +} + +export function getZori(): ZoriStore { + if (!globalStore) { + throw new Error('Zori not initialized. Call initZori(config) first.'); + } + return globalStore; +} + +// Action: trackClick +export function trackClick( + node: HTMLElement, + options: { eventName?: string; properties?: Record } = {} +) { + const handleClick = async () => { + const store = getZori(); + await store.track(options.eventName || 'click', options.properties || {}); + }; + + node.addEventListener('click', handleClick); + + return { + destroy() { + node.removeEventListener('click', handleClick); + }, + }; +} + +// Helper: usePageView (for components) +export function usePageView(properties?: Record) { + const store = getZori(); + + onMount(() => { + const unsubscribe = store.isInitialized.subscribe((initialized) => { + if (initialized) { + store.track('page_view', { + page_title: document.title, + page_path: window.location.pathname, + page_search: window.location.search, + page_hash: window.location.hash, + ...properties, + }); + unsubscribe(); + } + }); + }); +} + +// Helper: useTrackEvent (for components) +export function useTrackEvent(eventName: string, properties?: Record) { + const store = getZori(); + + onMount(() => { + const unsubscribe = store.isInitialized.subscribe((initialized) => { + if (initialized) { + store.track(eventName, properties); + unsubscribe(); + } + }); + }); +} + +// Helper: useIdentify (for components) +export function useIdentify(userInfo: UserInfo | null) { + const store = getZori(); + + onMount(() => { + if (!userInfo) return; + + const unsubscribe = store.isInitialized.subscribe((initialized) => { + if (initialized) { + store.identify(userInfo); + unsubscribe(); + } + }); + }); +} + +// Export default +export default { + createZoriStore, + initZori, + getZori, + trackClick, + usePageView, + useTrackEvent, + useIdentify, +}; diff --git a/svelte/package.json b/svelte/package.json new file mode 100644 index 0000000..3519db8 --- /dev/null +++ b/svelte/package.json @@ -0,0 +1,34 @@ +{ + "name": "@zorihq/svelte", + "version": "1.0.0", + "description": "ZoriHQ Analytics for Svelte", + "main": "dist/index.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts", + "svelte": "index.js", + "scripts": { + "build": "echo 'Build with your preferred bundler (rollup, svelte-package, etc.)'", + "test": "echo 'Tests not yet configured'" + }, + "keywords": [ + "analytics", + "svelte", + "sveltekit", + "tracking", + "zorihq" + ], + "author": "ZoriHQ", + "license": "MIT", + "peerDependencies": { + "svelte": ">=3.0.0" + }, + "devDependencies": { + "svelte": "^4.0.0", + "typescript": "^5.0.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/zorihq/script.git", + "directory": "svelte" + } +} diff --git a/vue/README.md b/vue/README.md new file mode 100644 index 0000000..71b97c7 --- /dev/null +++ b/vue/README.md @@ -0,0 +1,274 @@ +# @zorihq/vue + +Vue 3 composables and plugin for ZoriHQ Analytics. + +## Installation + +```bash +npm install @zorihq/vue +# or +pnpm add @zorihq/vue +# or +yarn add @zorihq/vue +``` + +## Usage + +### 1. Install the Plugin + +```typescript +// main.ts +import { createApp } from 'vue'; +import { ZoriPlugin } from '@zorihq/vue'; +import App from './App.vue'; +import router from './router'; // Optional: for auto-tracking + +const app = createApp(App); + +app.use(ZoriPlugin, { + config: { + publishableKey: 'your-publishable-key', + baseUrl: 'https://ingestion.zorihq.com/ingest', // optional + comebackThreshold: 30000, // optional + trackQuickSwitches: false, // optional + }, + router, // Optional: pass Vue Router for auto page view tracking + autoTrackPageViews: true, // Optional: default true +}); + +app.use(router); +app.mount('#app'); +``` + +### 2. Use the Composables + +#### useZori - Main Composable + +```vue + + + +``` + +#### usePageView - Auto Track Page Views + +```vue + + + +``` + +#### useIdentify - Auto Identify Users + +```vue + + + +``` + +#### useTrackEvent - Track Events with Reactivity + +```vue + + + +``` + +### 3. Template Usage with v-on + +```vue + + + +``` + +## Vue Router Integration + +When you pass the Vue Router instance to the plugin, it automatically tracks page views on route changes: + +```typescript +app.use(ZoriPlugin, { + config: { publishableKey: 'your-key' }, + router, // Auto-tracks page views + autoTrackPageViews: true, // Enable/disable auto-tracking +}); +``` + +Each route change will track: + +- `page_title`: Document title +- `page_path`: Route path +- `page_name`: Route name +- `page_search`: Query parameters (JSON stringified) + +## API Reference + +### ZoriPlugin Options + +```typescript +{ + config: { + publishableKey: string; // required + baseUrl?: string; // optional + comebackThreshold?: number; // optional + trackQuickSwitches?: boolean; // optional + }, + router?: Router; // optional Vue Router instance + autoTrackPageViews?: boolean; // optional, default true +} +``` + +### useZori() + +Returns an object with: + +- `isInitialized`: Readonly ref indicating if ZoriHQ is ready +- `track(eventName, properties)`: Track custom events +- `identify(userInfo)`: Identify users +- `getVisitorId()`: Get the visitor ID +- `getSessionId()`: Get the current session ID +- `setConsent(preferences)`: Set GDPR consent preferences +- `optOut()`: Opt out of tracking completely +- `hasConsent()`: Check if user has given consent + +### usePageView(properties?) + +Auto-track page view on component mount. Accepts static properties or reactive refs. + +### useTrackEvent(eventName, properties?) + +Track event on component mount. Supports reactive refs for both event name and properties. + +### useIdentify(userInfo) + +Identify user on component mount. Supports reactive refs. + +## TypeScript Support + +This package includes full TypeScript support: + +```typescript +import type { + ZoriConfig, + ZoriInstance, + ConsentPreferences, + UserInfo, +} from '@zorihq/vue'; +``` + +## Composition API + +All composables follow Vue 3 Composition API conventions and support: + +- Reactive refs +- Computed properties +- Automatic cleanup on unmount + +## Options API Support + +You can also use the plugin with Options API: + +```vue + +``` + +## License + +MIT diff --git a/vue/index.ts b/vue/index.ts new file mode 100644 index 0000000..bb23e15 --- /dev/null +++ b/vue/index.ts @@ -0,0 +1,313 @@ +import { + ref, + readonly, + onMounted, + onUnmounted, + watch, + inject, + provide, + type App, + type Ref, + type InjectionKey, +} from 'vue'; + +// Types +export interface ZoriConfig { + publishableKey: string; + baseUrl?: string; + comebackThreshold?: number; + trackQuickSwitches?: boolean; +} + +export interface ConsentPreferences { + analytics?: boolean; + marketing?: boolean; +} + +export interface UserInfo { + app_id?: string; + email?: string; + fullname?: string; + full_name?: string; + [key: string]: any; +} + +export interface ZoriInstance { + isInitialized: Readonly>; + track: (eventName: string, properties?: Record) => Promise; + identify: (userInfo: UserInfo) => Promise; + getVisitorId: () => Promise; + getSessionId: () => string | null; + setConsent: (preferences: ConsentPreferences) => boolean; + optOut: () => boolean; + hasConsent: () => boolean; +} + +// Injection Key +export const ZoriKey: InjectionKey = Symbol('zori'); + +// Plugin Options +export interface ZoriPluginOptions { + config: ZoriConfig; + router?: any; // Vue Router instance + autoTrackPageViews?: boolean; +} + +// Create Zori Instance +function createZoriInstance(config: ZoriConfig): ZoriInstance { + const isInitialized = ref(false); + let scriptLoaded = false; + + const loadScript = () => { + if (scriptLoaded || typeof window === 'undefined') return; + + // Initialize queue + (window as any).ZoriHQ = (window as any).ZoriHQ || []; + + // Load script + const script = document.createElement('script'); + script.src = 'https://cdn.zorihq.com/script.min.js'; + script.async = true; + script.setAttribute('data-key', config.publishableKey); + + if (config.baseUrl) { + script.setAttribute('data-base-url', config.baseUrl); + } + + if (config.comebackThreshold !== undefined) { + script.setAttribute('data-comeback-threshold', config.comebackThreshold.toString()); + } + + if (config.trackQuickSwitches !== undefined) { + script.setAttribute('data-track-quick-switches', config.trackQuickSwitches.toString()); + } + + script.onload = () => { + isInitialized.value = true; + }; + + document.head.appendChild(script); + scriptLoaded = true; + }; + + const track = async (eventName: string, properties?: Record): Promise => { + const zori = (window as any).ZoriHQ; + if (!zori) return false; + + if (typeof zori.track === 'function') { + return await zori.track(eventName, properties); + } else { + zori.push(['track', eventName, properties]); + return true; + } + }; + + const identify = async (userInfo: UserInfo): Promise => { + const zori = (window as any).ZoriHQ; + if (!zori) return false; + + if (typeof zori.identify === 'function') { + return await zori.identify(userInfo); + } else { + zori.push(['identify', userInfo]); + return true; + } + }; + + const getVisitorId = async (): Promise => { + const zori = (window as any).ZoriHQ; + if (!zori) return ''; + + if (typeof zori.getVisitorId === 'function') { + return await zori.getVisitorId(); + } + + return new Promise((resolve) => { + zori.push(['getVisitorId', (id: string) => resolve(id)]); + }); + }; + + const getSessionId = (): string | null => { + const zori = (window as any).ZoriHQ; + if (!zori || typeof zori.getSessionId !== 'function') return null; + return zori.getSessionId(); + }; + + const setConsent = (preferences: ConsentPreferences): boolean => { + const zori = (window as any).ZoriHQ; + if (!zori) return false; + + if (typeof zori.setConsent === 'function') { + return zori.setConsent(preferences); + } else { + zori.push(['setConsent', preferences]); + return true; + } + }; + + const optOut = (): boolean => { + const zori = (window as any).ZoriHQ; + if (!zori) return false; + + if (typeof zori.optOut === 'function') { + return zori.optOut(); + } else { + zori.push(['optOut']); + return true; + } + }; + + const hasConsent = (): boolean => { + const zori = (window as any).ZoriHQ; + if (!zori || typeof zori.hasConsent !== 'function') return true; + return zori.hasConsent(); + }; + + // Load script immediately + if (typeof window !== 'undefined') { + loadScript(); + } + + return { + isInitialized: readonly(isInitialized), + track, + identify, + getVisitorId, + getSessionId, + setConsent, + optOut, + hasConsent, + }; +} + +// Vue Plugin +export const ZoriPlugin = { + install(app: App, options: ZoriPluginOptions) { + const zoriInstance = createZoriInstance(options.config); + app.provide(ZoriKey, zoriInstance); + + // Auto-track page views with Vue Router + if (options.router && options.autoTrackPageViews !== false) { + options.router.afterEach((to: any) => { + if (zoriInstance.isInitialized.value) { + zoriInstance.track('page_view', { + page_title: document.title, + page_path: to.path, + page_name: to.name, + page_search: to.query ? JSON.stringify(to.query) : '', + }); + } + }); + } + }, +}; + +// Composable: useZori +export function useZori(): ZoriInstance { + const zori = inject(ZoriKey); + if (!zori) { + throw new Error('useZori must be used within a component with ZoriPlugin installed'); + } + return zori; +} + +// Composable: usePageView +export function usePageView(properties?: Ref> | Record) { + const { track, isInitialized } = useZori(); + + onMounted(() => { + if (isInitialized.value) { + const props = typeof properties === 'object' && 'value' in properties ? properties.value : properties; + track('page_view', { + page_title: document.title, + page_path: window.location.pathname, + page_search: window.location.search, + page_hash: window.location.hash, + ...props, + }); + } + }); + + // Watch for changes if properties is a ref + if (properties && typeof properties === 'object' && 'value' in properties) { + watch( + properties, + (newProps) => { + if (isInitialized.value) { + track('page_view', { + page_title: document.title, + page_path: window.location.pathname, + page_search: window.location.search, + page_hash: window.location.hash, + ...newProps, + }); + } + }, + { deep: true } + ); + } +} + +// Composable: useTrackEvent +export function useTrackEvent( + eventName: string | Ref, + properties?: Ref> | Record +) { + const { track, isInitialized } = useZori(); + + onMounted(() => { + if (isInitialized.value) { + const name = typeof eventName === 'string' ? eventName : eventName.value; + const props = typeof properties === 'object' && 'value' in properties ? properties.value : properties; + track(name, props); + } + }); + + // Watch for changes + watch( + [ + typeof eventName === 'string' ? ref(eventName) : eventName, + typeof properties === 'object' && 'value' in properties ? properties : ref(properties), + ], + ([newName, newProps]) => { + if (isInitialized.value) { + track(newName as string, newProps as Record); + } + }, + { deep: true } + ); +} + +// Composable: useIdentify +export function useIdentify(userInfo: Ref | UserInfo | null) { + const { identify, isInitialized } = useZori(); + + onMounted(() => { + const info = typeof userInfo === 'object' && 'value' in userInfo ? userInfo.value : userInfo; + if (isInitialized.value && info) { + identify(info); + } + }); + + // Watch for changes if userInfo is a ref + if (userInfo && typeof userInfo === 'object' && 'value' in userInfo) { + watch( + userInfo, + (newInfo) => { + if (isInitialized.value && newInfo) { + identify(newInfo); + } + }, + { deep: true } + ); + } +} + +// Export default +export default { + ZoriPlugin, + useZori, + usePageView, + useTrackEvent, + useIdentify, +}; diff --git a/vue/package.json b/vue/package.json new file mode 100644 index 0000000..51c95b8 --- /dev/null +++ b/vue/package.json @@ -0,0 +1,33 @@ +{ + "name": "@zorihq/vue", + "version": "1.0.0", + "description": "ZoriHQ Analytics for Vue.js", + "main": "dist/index.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "echo 'Build with your preferred bundler (rollup, tsup, etc.)'", + "test": "echo 'Tests not yet configured'" + }, + "keywords": [ + "analytics", + "vue", + "vue3", + "tracking", + "zorihq" + ], + "author": "ZoriHQ", + "license": "MIT", + "peerDependencies": { + "vue": ">=3.0.0" + }, + "devDependencies": { + "typescript": "^5.0.0", + "vue": "^3.0.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/zorihq/script.git", + "directory": "vue" + } +} From 3df93b08afa745cf1038d51d22507f3bb8f52ada Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Nov 2025 12:29:50 +0000 Subject: [PATCH 2/4] feat: add npm publishing workflow and build configurations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added comprehensive npm publishing infrastructure: Workflow Features: - Unified GitHub Actions workflow for publishing all framework packages - Support for publishing individual packages or all at once - Semantic versioning with patch/minor/major options - Dry-run mode for safe testing before actual publish - Matrix strategy for parallel publishing when selecting 'all' - Automatic version bumping and git tagging - GitHub Release creation with installation instructions Package Updates: - Added tsconfig.json to all packages (React, Next.js, Next.js Server, Vue, Svelte) - Updated package.json with proper exports field and files list - Publishing source files directly (TypeScript/TSX) for user build systems - Simplified build process with tsc support Documentation: - Comprehensive PUBLISHING.md guide with: - Step-by-step publishing instructions - NPM token setup guide - Manual and automated publishing methods - Version bump guidelines (semantic versioning) - Troubleshooting section - Best practices Security: - Uses NPM_TOKEN secret for authentication - Requires proper GitHub permissions (contents:write, packages:write) - Supports dry-run testing before actual publish Usage: Run via GitHub Actions → Publish Framework Packages workflow Select package (or 'all'), version bump type, and dry-run option --- .github/workflows/publish-packages.yml | 154 +++++++++++++++++ PUBLISHING.md | 228 +++++++++++++++++++++++++ nextjs-server/package.json | 20 ++- nextjs-server/tsconfig.json | 20 +++ nextjs/package.json | 20 ++- nextjs/tsconfig.json | 21 +++ react/package.json | 20 ++- react/tsconfig.json | 21 +++ svelte/package.json | 25 ++- svelte/tsconfig.json | 20 +++ vue/package.json | 20 ++- vue/tsconfig.json | 20 +++ 12 files changed, 568 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/publish-packages.yml create mode 100644 PUBLISHING.md create mode 100644 nextjs-server/tsconfig.json create mode 100644 nextjs/tsconfig.json create mode 100644 react/tsconfig.json create mode 100644 svelte/tsconfig.json create mode 100644 vue/tsconfig.json diff --git a/.github/workflows/publish-packages.yml b/.github/workflows/publish-packages.yml new file mode 100644 index 0000000..dbeb918 --- /dev/null +++ b/.github/workflows/publish-packages.yml @@ -0,0 +1,154 @@ +name: Publish Framework Packages + +on: + workflow_dispatch: + inputs: + package: + description: 'Package to publish (react, nextjs, nextjs-server, vue, svelte, or all)' + required: true + type: choice + options: + - all + - react + - nextjs + - nextjs-server + - vue + - svelte + version: + description: 'Version bump type' + required: true + type: choice + options: + - patch + - minor + - major + dry-run: + description: 'Dry run (do not publish)' + required: false + type: boolean + default: false + +jobs: + publish: + runs-on: ubuntu-latest + strategy: + matrix: + package: ${{ github.event.inputs.package == 'all' && fromJSON('["react", "nextjs", "nextjs-server", "vue", "svelte"]') || fromJSON(format('["{0}"]', github.event.inputs.package)) }} + + permissions: + contents: write + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }} + + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + version: 10 + run_install: false + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'pnpm' + registry-url: 'https://registry.npmjs.org' + + - name: Install root dependencies + run: pnpm install --frozen-lockfile || pnpm install + + - name: Install package dependencies + working-directory: ./${{ matrix.package }} + run: pnpm install || npm install + + - name: Verify package files + working-directory: ./${{ matrix.package }} + run: | + echo "📦 Package contents:" + ls -la + + echo "" + echo "📄 Files to be published:" + npm pack --dry-run 2>&1 | grep -A 100 "package:" + + - name: Bump version + working-directory: ./${{ matrix.package }} + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + # Get current version + CURRENT_VERSION=$(node -p "require('./package.json').version") + echo "Current version: $CURRENT_VERSION" + + # Bump version + npm version ${{ github.event.inputs.version }} --no-git-tag-version + + # Get new version + NEW_VERSION=$(node -p "require('./package.json').version") + echo "New version: $NEW_VERSION" + echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV + + - name: Verify version update + working-directory: ./${{ matrix.package }} + run: | + echo "✅ Version updated to: ${{ env.NEW_VERSION }}" + cat package.json | grep version + + - name: Publish to npm (Dry Run) + if: github.event.inputs.dry-run == 'true' + working-directory: ./${{ matrix.package }} + run: | + echo "🔍 Dry run mode - would publish:" + npm pack --dry-run + ls -la + + - name: Publish to npm + if: github.event.inputs.dry-run != 'true' + working-directory: ./${{ matrix.package }} + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + # Publish from root directory (includes source files) + npm publish --access public + + - name: Commit version bump + if: github.event.inputs.dry-run != 'true' + run: | + git add ${{ matrix.package }}/package.json + git commit -m "chore(${{ matrix.package }}): release v${{ env.NEW_VERSION }}" || echo "No changes to commit" + git push origin ${{ github.ref_name }} || echo "Nothing to push" + + - name: Create git tag + if: github.event.inputs.dry-run != 'true' + run: | + TAG_NAME="${{ matrix.package }}/v${{ env.NEW_VERSION }}" + git tag -a "$TAG_NAME" -m "Release ${{ matrix.package }} v${{ env.NEW_VERSION }}" + git push origin "$TAG_NAME" + env: + GITHUB_TOKEN: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }} + + - name: Create GitHub Release + if: github.event.inputs.dry-run != 'true' + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ matrix.package }}/v${{ env.NEW_VERSION }} + release_name: ${{ matrix.package }} v${{ env.NEW_VERSION }} + body: | + Release of @zorihq/${{ matrix.package }} v${{ env.NEW_VERSION }} + + ## Installation + ```bash + npm install @zorihq/${{ matrix.package }}@${{ env.NEW_VERSION }} + ``` + + See the [README](${{ github.server_url }}/${{ github.repository }}/tree/main/${{ matrix.package }}/README.md) for usage instructions. + draft: false + prerelease: false diff --git a/PUBLISHING.md b/PUBLISHING.md new file mode 100644 index 0000000..ed859a2 --- /dev/null +++ b/PUBLISHING.md @@ -0,0 +1,228 @@ +# Publishing Framework Libraries + +This document describes how to publish the ZoriHQ framework libraries to npm. + +## Prerequisites + +### 1. NPM Token + +You need to configure an NPM authentication token in GitHub Secrets: + +1. Create an npm account at https://www.npmjs.com/ +2. Generate an automation token: https://www.npmjs.com/settings/YOUR_USERNAME/tokens +3. Add it to GitHub repository secrets as `NPM_TOKEN` + +### 2. Package Scope + +All packages are published under the `@zorihq` scope: +- `@zorihq/react` +- `@zorihq/nextjs` +- `@zorihq/nextjs-server` +- `@zorihq/vue` +- `@zorihq/svelte` + +Ensure you have permission to publish to this scope on npm. + +## Publishing Workflow + +### Automatic Publishing via GitHub Actions + +The workflow `publish-packages.yml` handles building, versioning, and publishing packages to npm. + +#### Publish a Single Package + +1. Go to **Actions** → **Publish Framework Packages** +2. Click **Run workflow** +3. Select options: + - **Package**: Choose which package to publish (react, nextjs, nextjs-server, vue, svelte) + - **Version bump type**: Choose `patch`, `minor`, or `major` + - **Dry run**: Check this to test without actually publishing + +#### Publish All Packages + +1. Go to **Actions** → **Publish Framework Packages** +2. Click **Run workflow** +3. Select options: + - **Package**: Choose `all` + - **Version bump type**: Choose `patch`, `minor`, or `major` + - **Dry run**: Check this to test without actually publishing + +### What the Workflow Does + +1. **Checks out code** from the repository +2. **Installs dependencies** for the selected package(s) +3. **Verifies package files** to ensure everything is ready +4. **Bumps version** according to your selection (patch, minor, major) +5. **Publishes to npm** with public access +6. **Commits version bump** back to the repository +7. **Creates git tag** in format `{package}/v{version}` (e.g., `react/v1.0.1`) +8. **Creates GitHub Release** with installation instructions + +## Manual Publishing (Local) + +If you need to publish manually from your local machine: + +### 1. Login to npm + +```bash +npm login +``` + +### 2. Navigate to Package Directory + +```bash +cd react # or nextjs, nextjs-server, vue, svelte +``` + +### 3. Bump Version + +```bash +npm version patch # or minor, major +``` + +### 4. Publish + +```bash +npm publish --access public +``` + +### 5. Tag and Push + +```bash +git add package.json +git commit -m "chore(react): release v1.0.1" +git tag react/v1.0.1 +git push origin main --tags +``` + +## Version Bump Guidelines + +Follow semantic versioning: + +- **patch** (1.0.0 → 1.0.1): Bug fixes, small improvements +- **minor** (1.0.0 → 1.1.0): New features, backward compatible +- **major** (1.0.0 → 2.0.0): Breaking changes + +## Package Structure + +Each package is published with source files (TypeScript/TSX) to allow users' build systems to handle compilation: + +``` +@zorihq/react/ +├── index.tsx # Main source file +├── README.md # Usage documentation +├── package.json # Package manifest +└── tsconfig.json # TypeScript config +``` + +## Verifying Published Packages + +After publishing, verify on npm: + +```bash +npm view @zorihq/react +npm view @zorihq/nextjs +npm view @zorihq/nextjs-server +npm view @zorihq/vue +npm view @zorihq/svelte +``` + +Or visit: +- https://www.npmjs.com/package/@zorihq/react +- https://www.npmjs.com/package/@zorihq/nextjs +- https://www.npmjs.com/package/@zorihq/nextjs-server +- https://www.npmjs.com/package/@zorihq/vue +- https://www.npmjs.com/package/@zorihq/svelte + +## Testing Before Publishing + +### Dry Run with GitHub Actions + +Always test with dry-run enabled first: + +1. Run the workflow with **dry-run: true** +2. Check the workflow logs to see what would be published +3. If everything looks good, run again with **dry-run: false** + +### Local Testing + +Test package locally before publishing: + +```bash +cd react +npm pack +# This creates a .tgz file you can inspect or install locally +``` + +Install the packed version in a test project: + +```bash +npm install /path/to/zorihq-react-1.0.0.tgz +``` + +## Troubleshooting + +### "You do not have permission to publish" + +- Ensure you're logged into the correct npm account +- Ensure you have access to the `@zorihq` scope +- Check that `NPM_TOKEN` secret is correctly configured + +### "Version already exists" + +- You're trying to publish a version that's already on npm +- Bump the version first with `npm version patch/minor/major` + +### "Files not included in package" + +- Check the `files` field in `package.json` +- Run `npm pack --dry-run` to see what would be included +- Verify source files exist in the package directory + +## CI/CD Integration + +The workflow integrates with GitHub's CI/CD: + +- **On workflow_dispatch**: Manual trigger from GitHub Actions UI +- **Permissions**: Requires `contents: write` and `packages: write` +- **Matrix strategy**: Can publish multiple packages in parallel when `all` is selected + +## Post-Publishing + +After publishing: + +1. **Update documentation** if needed +2. **Announce release** in relevant channels +3. **Monitor npm** for download stats +4. **Watch for issues** reported by users + +## Best Practices + +1. ✅ **Always use dry-run first** for new packages +2. ✅ **Test locally** before publishing +3. ✅ **Follow semantic versioning** strictly +4. ✅ **Update README** with breaking changes +5. ✅ **Tag releases** with descriptive names +6. ✅ **Keep changelogs** updated +7. ✅ **Coordinate releases** when updating multiple packages + +## Package URLs + +After publishing, packages are available at: + +- **npm**: `https://www.npmjs.com/package/@zorihq/{package-name}` +- **GitHub**: `https://github.com/ZoriHQ/script/tree/main/{package-name}` +- **Unpkg CDN**: `https://unpkg.com/@zorihq/{package-name}` (for bundled builds) + +## Support + +For issues with publishing: + +1. Check GitHub Actions logs for detailed error messages +2. Verify npm credentials and permissions +3. Review package.json configuration +4. Test with `npm pack --dry-run` locally + +## License + +All packages are published under the MIT License. diff --git a/nextjs-server/package.json b/nextjs-server/package.json index 10f2240..89ab7a9 100644 --- a/nextjs-server/package.json +++ b/nextjs-server/package.json @@ -2,11 +2,23 @@ "name": "@zorihq/nextjs-server", "version": "1.0.0", "description": "ZoriHQ Analytics for Next.js (Server-side)", - "main": "dist/index.js", - "module": "dist/index.esm.js", - "types": "dist/index.d.ts", + "main": "index.ts", + "types": "index.ts", + "exports": { + ".": { + "import": "./index.ts", + "require": "./index.ts", + "types": "./index.ts" + } + }, + "files": [ + "index.ts", + "README.md", + "package.json" + ], "scripts": { - "build": "echo 'Build with your preferred bundler (rollup, tsup, etc.)'", + "build": "tsc", + "prepublishOnly": "echo 'Publishing source files'", "test": "echo 'Tests not yet configured'" }, "keywords": [ diff --git a/nextjs-server/tsconfig.json b/nextjs-server/tsconfig.json new file mode 100644 index 0000000..4503eb5 --- /dev/null +++ b/nextjs-server/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020"], + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true + }, + "include": ["index.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/nextjs/package.json b/nextjs/package.json index ee19276..dff7850 100644 --- a/nextjs/package.json +++ b/nextjs/package.json @@ -2,11 +2,23 @@ "name": "@zorihq/nextjs", "version": "1.0.0", "description": "ZoriHQ Analytics for Next.js (Client-side)", - "main": "dist/index.js", - "module": "dist/index.esm.js", - "types": "dist/index.d.ts", + "main": "index.tsx", + "types": "index.tsx", + "exports": { + ".": { + "import": "./index.tsx", + "require": "./index.tsx", + "types": "./index.tsx" + } + }, + "files": [ + "index.tsx", + "README.md", + "package.json" + ], "scripts": { - "build": "echo 'Build with your preferred bundler (rollup, tsup, etc.)'", + "build": "tsc", + "prepublishOnly": "echo 'Publishing source files'", "test": "echo 'Tests not yet configured'" }, "keywords": [ diff --git a/nextjs/tsconfig.json b/nextjs/tsconfig.json new file mode 100644 index 0000000..eee058f --- /dev/null +++ b/nextjs/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "jsx": "react", + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true + }, + "include": ["index.tsx"], + "exclude": ["node_modules", "dist"] +} diff --git a/react/package.json b/react/package.json index 0244755..19e4063 100644 --- a/react/package.json +++ b/react/package.json @@ -2,11 +2,23 @@ "name": "@zorihq/react", "version": "1.0.0", "description": "ZoriHQ Analytics for React", - "main": "dist/index.js", - "module": "dist/index.esm.js", - "types": "dist/index.d.ts", + "main": "index.tsx", + "types": "index.tsx", + "exports": { + ".": { + "import": "./index.tsx", + "require": "./index.tsx", + "types": "./index.tsx" + } + }, + "files": [ + "index.tsx", + "README.md", + "package.json" + ], "scripts": { - "build": "echo 'Build with your preferred bundler (rollup, tsup, etc.)'", + "build": "tsc", + "prepublishOnly": "echo 'Publishing source files'", "test": "echo 'Tests not yet configured'" }, "keywords": [ diff --git a/react/tsconfig.json b/react/tsconfig.json new file mode 100644 index 0000000..eee058f --- /dev/null +++ b/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "jsx": "react", + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true + }, + "include": ["index.tsx"], + "exclude": ["node_modules", "dist"] +} diff --git a/svelte/package.json b/svelte/package.json index 3519db8..e9e926a 100644 --- a/svelte/package.json +++ b/svelte/package.json @@ -2,12 +2,27 @@ "name": "@zorihq/svelte", "version": "1.0.0", "description": "ZoriHQ Analytics for Svelte", - "main": "dist/index.js", - "module": "dist/index.esm.js", - "types": "dist/index.d.ts", - "svelte": "index.js", + "main": "index.ts", + "svelte": "index.ts", + "types": "index.ts", + "exports": { + ".": { + "import": "./index.ts", + "require": "./index.ts", + "types": "./index.ts", + "svelte": "./index.ts" + }, + "./ZoriProvider.svelte": "./ZoriProvider.svelte" + }, + "files": [ + "index.ts", + "ZoriProvider.svelte", + "README.md", + "package.json" + ], "scripts": { - "build": "echo 'Build with your preferred bundler (rollup, svelte-package, etc.)'", + "build": "tsc", + "prepublishOnly": "echo 'Publishing source files'", "test": "echo 'Tests not yet configured'" }, "keywords": [ diff --git a/svelte/tsconfig.json b/svelte/tsconfig.json new file mode 100644 index 0000000..23585ec --- /dev/null +++ b/svelte/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true + }, + "include": ["index.ts"], + "exclude": ["node_modules", "dist", "*.svelte"] +} diff --git a/vue/package.json b/vue/package.json index 51c95b8..8178240 100644 --- a/vue/package.json +++ b/vue/package.json @@ -2,11 +2,23 @@ "name": "@zorihq/vue", "version": "1.0.0", "description": "ZoriHQ Analytics for Vue.js", - "main": "dist/index.js", - "module": "dist/index.esm.js", - "types": "dist/index.d.ts", + "main": "index.ts", + "types": "index.ts", + "exports": { + ".": { + "import": "./index.ts", + "require": "./index.ts", + "types": "./index.ts" + } + }, + "files": [ + "index.ts", + "README.md", + "package.json" + ], "scripts": { - "build": "echo 'Build with your preferred bundler (rollup, tsup, etc.)'", + "build": "tsc", + "prepublishOnly": "echo 'Publishing source files'", "test": "echo 'Tests not yet configured'" }, "keywords": [ diff --git a/vue/tsconfig.json b/vue/tsconfig.json new file mode 100644 index 0000000..3517570 --- /dev/null +++ b/vue/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true + }, + "include": ["index.ts"], + "exclude": ["node_modules", "dist"] +} From d98bd3af4c684c51e40f7c45a162dff900f4c1b0 Mon Sep 17 00:00:00 2001 From: Vladyslav Len Date: Fri, 21 Nov 2025 20:15:30 +0100 Subject: [PATCH 3/4] chore(): updates --- svelte/index.ts | 32 ++------ svelte/package.json | 3 + svelte/pnpm-lock.yaml | 183 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 191 insertions(+), 27 deletions(-) create mode 100644 svelte/pnpm-lock.yaml diff --git a/svelte/index.ts b/svelte/index.ts index 4ebb3e5..fa2900b 100644 --- a/svelte/index.ts +++ b/svelte/index.ts @@ -1,36 +1,14 @@ import { writable, readonly, derived, get, type Readable, type Writable } from 'svelte/store'; import { onMount, onDestroy } from 'svelte'; +import type { ZoriConfig, ConsentPreferences, UserInfo, ZoriCoreAPI } from '@zorihq/types'; -// Types -export interface ZoriConfig { - publishableKey: string; - baseUrl?: string; - comebackThreshold?: number; - trackQuickSwitches?: boolean; -} - -export interface ConsentPreferences { - analytics?: boolean; - marketing?: boolean; -} - -export interface UserInfo { - app_id?: string; - email?: string; - fullname?: string; - full_name?: string; - [key: string]: any; -} +// Re-export shared types for convenience +export type { ZoriConfig, ConsentPreferences, UserInfo } from '@zorihq/types'; -export interface ZoriStore { +// Svelte-specific store type extending core API with reactive state +export interface ZoriStore extends Omit { isInitialized: Readable; - track: (eventName: string, properties?: Record) => Promise; - identify: (userInfo: UserInfo) => Promise; - getVisitorId: () => Promise; getSessionId: () => string | null; - setConsent: (preferences: ConsentPreferences) => boolean; - optOut: () => boolean; - hasConsent: () => boolean; } // Create the Zori store diff --git a/svelte/package.json b/svelte/package.json index e9e926a..0a02e01 100644 --- a/svelte/package.json +++ b/svelte/package.json @@ -37,6 +37,9 @@ "peerDependencies": { "svelte": ">=3.0.0" }, + "dependencies": { + "@zorihq/types": "^1.0.0" + }, "devDependencies": { "svelte": "^4.0.0", "typescript": "^5.0.0" diff --git a/svelte/pnpm-lock.yaml b/svelte/pnpm-lock.yaml new file mode 100644 index 0000000..cf485d1 --- /dev/null +++ b/svelte/pnpm-lock.yaml @@ -0,0 +1,183 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@zorihq/types': + specifier: ^1.0.0 + version: 1.0.0 + devDependencies: + svelte: + specifier: ^4.0.0 + version: 4.2.20 + typescript: + specifier: ^5.0.0 + version: 5.9.3 + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@zorihq/types@1.0.0': + resolution: {integrity: sha512-5XcYod8Pbz34iAkIWBxnFop4djdzj8z5JDj47uOh3Xsj453ok7lpvl2DjcQOcVoaWRn6F69GjM6jQBzulOxHDg==} + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + code-red@1.0.4: + resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==} + + css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + + periscopic@3.1.0: + resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + svelte@4.2.20: + resolution: {integrity: sha512-eeEgGc2DtiUil5ANdtd8vPwt9AgaMdnuUFnPft9F5oMvU/FHu5IHFic+p1dR/UOB7XU2mX2yHW+NcTch4DCh5Q==} + engines: {node: '>=16'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@types/estree@1.0.8': {} + + '@zorihq/types@1.0.0': {} + + acorn@8.15.0: {} + + aria-query@5.3.2: {} + + axobject-query@4.1.0: {} + + code-red@1.0.4: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@types/estree': 1.0.8 + acorn: 8.15.0 + estree-walker: 3.0.3 + periscopic: 3.1.0 + + css-tree@2.3.1: + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.2.1 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + locate-character@3.0.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + mdn-data@2.0.30: {} + + periscopic@3.1.0: + dependencies: + '@types/estree': 1.0.8 + estree-walker: 3.0.3 + is-reference: 3.0.3 + + source-map-js@1.2.1: {} + + svelte@4.2.20: + dependencies: + '@ampproject/remapping': 2.3.0 + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + '@types/estree': 1.0.8 + acorn: 8.15.0 + aria-query: 5.3.2 + axobject-query: 4.1.0 + code-red: 1.0.4 + css-tree: 2.3.1 + estree-walker: 3.0.3 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.21 + periscopic: 3.1.0 + + typescript@5.9.3: {} From 578b94b4ae68ebeca9ad93489c3f716344100a29 Mon Sep 17 00:00:00 2001 From: Vladyslav Len Date: Fri, 21 Nov 2025 20:15:42 +0100 Subject: [PATCH 4/4] chore(): work --- nextjs-server/README.md | 302 --------------------------------- nextjs-server/index.ts | 287 -------------------------------- nextjs-server/package.json | 46 ------ nextjs-server/tsconfig.json | 20 --- nextjs/index.tsx | 33 +--- nextjs/package.json | 3 + nextjs/pnpm-lock.yaml | 321 ++++++++++++++++++++++++++++++++++++ react/index.tsx | 33 +--- react/package.json | 3 + react/pnpm-lock.yaml | 78 +++++++++ shared/package.json | 34 ++++ shared/types.ts | 132 +++++++++++++++ vue/index.ts | 163 +++++++++--------- vue/package.json | 3 + vue/pnpm-lock.yaml | 223 +++++++++++++++++++++++++ 15 files changed, 883 insertions(+), 798 deletions(-) delete mode 100644 nextjs-server/README.md delete mode 100644 nextjs-server/index.ts delete mode 100644 nextjs-server/package.json delete mode 100644 nextjs-server/tsconfig.json create mode 100644 nextjs/pnpm-lock.yaml create mode 100644 react/pnpm-lock.yaml create mode 100644 shared/package.json create mode 100644 shared/types.ts create mode 100644 vue/pnpm-lock.yaml diff --git a/nextjs-server/README.md b/nextjs-server/README.md deleted file mode 100644 index 9e5b266..0000000 --- a/nextjs-server/README.md +++ /dev/null @@ -1,302 +0,0 @@ -# @zorihq/nextjs-server - -Server-side tracking for Next.js with ZoriHQ Analytics. Use this in Server Components, API Routes, Server Actions, and Middleware. - -## Installation - -```bash -npm install @zorihq/nextjs-server -# or -pnpm add @zorihq/nextjs-server -# or -yarn add @zorihq/nextjs-server -``` - -## Usage - -### 1. Initialize the Client - -Create `lib/zori-server.ts`: - -```typescript -import { createZoriServer } from '@zorihq/nextjs-server'; - -export const zoriServer = createZoriServer({ - publishableKey: process.env.ZORI_PUBLISHABLE_KEY!, - baseUrl: 'https://ingestion.zorihq.com/ingest', // optional -}); -``` - -### 2. Track Events in Server Components - -```tsx -import { zoriServer } from '@/lib/zori-server'; - -export default async function ProductPage({ params }: { params: { id: string } }) { - // Track page view - await zoriServer.track({ - eventName: 'product_viewed', - properties: { - product_id: params.id, - page_type: 'product', - }, - }); - - return
Product {params.id}
; -} -``` - -### 3. Track Events in API Routes (App Router) - -```typescript -// app/api/purchase/route.ts -import { NextResponse } from 'next/server'; -import { zoriServer } from '@/lib/zori-server'; - -export async function POST(request: Request) { - const body = await request.json(); - - // Track purchase event - await zoriServer.track({ - eventName: 'purchase_completed', - properties: { - product_id: body.productId, - amount: body.amount, - }, - }); - - return NextResponse.json({ success: true }); -} -``` - -### 4. Track Events in API Routes (Pages Router) - -```typescript -// pages/api/purchase.ts -import type { NextApiRequest, NextApiResponse } from 'next'; -import { zoriServer } from '@/lib/zori-server'; - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - if (req.method === 'POST') { - await zoriServer.track({ - eventName: 'purchase_completed', - properties: { - product_id: req.body.productId, - amount: req.body.amount, - }, - }); - - res.status(200).json({ success: true }); - } -} -``` - -### 5. Identify Users in Server Actions - -```typescript -// app/actions.ts -'use server'; - -import { zoriServer } from '@/lib/zori-server'; - -export async function loginUser(email: string, userId: string) { - // Identify user - await zoriServer.identify({ - userInfo: { - app_id: userId, - email: email, - fullname: 'John Doe', - }, - }); - - // Track login event - await zoriServer.track({ - eventName: 'user_login', - properties: { - method: 'email', - }, - }); - - return { success: true }; -} -``` - -### 6. Track in Middleware - -```typescript -// middleware.ts -import { NextResponse } from 'next/server'; -import type { NextRequest } from 'next/server'; -import { createZoriServer } from '@zorihq/nextjs-server'; - -const zori = createZoriServer({ - publishableKey: process.env.ZORI_PUBLISHABLE_KEY!, -}); - -export async function middleware(request: NextRequest) { - // Track middleware events (e.g., auth checks, redirects) - await zori.track({ - eventName: 'middleware_check', - properties: { - path: request.nextUrl.pathname, - }, - pageUrl: request.url, - }); - - return NextResponse.next(); -} - -export const config = { - matcher: '/dashboard/:path*', -}; -``` - -## API Reference - -### ZoriServer Class - -#### Constructor - -```typescript -const zori = new ZoriServer({ - publishableKey: 'your-key', // required - baseUrl: 'https://ingestion.zorihq.com/ingest', // optional -}); -``` - -#### Methods - -##### `track(options: TrackEventOptions): Promise` - -Track a custom event. - -```typescript -await zori.track({ - eventName: 'button_clicked', - properties: { - button_name: 'signup', - location: 'header', - }, - // Optional overrides: - visitorId: 'custom-visitor-id', - sessionId: 'custom-session-id', - userAgent: 'custom-user-agent', - pageUrl: 'https://example.com/page', - host: 'example.com', - referrer: 'https://google.com', -}); -``` - -##### `identify(options: IdentifyOptions): Promise` - -Identify a user. - -```typescript -await zori.identify({ - userInfo: { - app_id: 'user_123', - email: 'user@example.com', - fullname: 'John Doe', - plan: 'premium', // Custom properties - signup_date: '2025-01-15', - }, - // Optional overrides: - visitorId: 'custom-visitor-id', - sessionId: 'custom-session-id', - userAgent: 'custom-user-agent', - pageUrl: 'https://example.com/page', - host: 'example.com', -}); -``` - -##### `getVisitorId(): Promise` - -Get the current visitor ID from cookies. - -```typescript -const visitorId = await zori.getVisitorId(); -``` - -##### `getSessionId(): Promise` - -Get the current session ID from cookies. - -```typescript -const sessionId = await zori.getSessionId(); -``` - -##### `getOrCreateVisitorId(): Promise` - -Get or create a visitor ID (sets cookie if not exists). - -```typescript -const visitorId = await zori.getOrCreateVisitorId(); -``` - -##### `getOrCreateSessionId(): Promise` - -Get or create a session ID (sets cookie if not exists). - -```typescript -const sessionId = await zori.getOrCreateSessionId(); -``` - -## Features - -### Automatic Cookie Management - -The library automatically manages visitor and session cookies: - -- `zori_visitor_id` - 2 year expiry -- `zori_session_id` - Browser session expiry - -### Automatic Request Metadata - -Automatically extracts from Next.js headers: - -- User-Agent -- Referrer -- Host - -### UTM Parameter Tracking - -Automatically extracts UTM parameters from the `pageUrl` option. - -## Environment Variables - -Create `.env.local`: - -```env -ZORI_PUBLISHABLE_KEY=your-publishable-key -``` - -## Best Practices - -1. **Initialize once**: Create a single instance and reuse it across your app -2. **Async/await**: Always await tracking calls in critical paths -3. **Error handling**: Tracking failures won't throw errors but return `false` -4. **Middleware**: Keep middleware tracking lightweight to avoid latency - -## TypeScript Support - -This package includes full TypeScript definitions. - -```typescript -import type { - ZoriConfig, - TrackEventOptions, - IdentifyOptions, - UserInfo, -} from '@zorihq/nextjs-server'; -``` - -## Client-Side Tracking - -For client-side tracking (hooks, components, browser events), use `@zorihq/nextjs` instead. - -## License - -MIT diff --git a/nextjs-server/index.ts b/nextjs-server/index.ts deleted file mode 100644 index d5a31fa..0000000 --- a/nextjs-server/index.ts +++ /dev/null @@ -1,287 +0,0 @@ -import { cookies, headers } from 'next/headers'; - -const VERSION = '1.0.0'; -const COOKIE_NAME = 'zori_visitor_id'; -const SESSION_COOKIE_NAME = 'zori_session_id'; -const COOKIE_EXPIRY_DAYS = 365 * 2; // 2 years -const DEFAULT_API_URL = 'https://ingestion.zorihq.com/ingest'; - -// Types -export interface ZoriConfig { - publishableKey: string; - baseUrl?: string; -} - -export interface UserInfo { - app_id?: string; - email?: string; - fullname?: string; - full_name?: string; - [key: string]: any; -} - -export interface TrackEventOptions { - eventName: string; - properties?: Record; - visitorId?: string; - sessionId?: string; - userAgent?: string; - pageUrl?: string; - host?: string; - referrer?: string; -} - -export interface IdentifyOptions { - userInfo: UserInfo; - visitorId?: string; - sessionId?: string; - userAgent?: string; - pageUrl?: string; - host?: string; -} - -// Main Zori Server Class -export class ZoriServer { - private config: Required; - - constructor(config: ZoriConfig) { - this.config = { - publishableKey: config.publishableKey, - baseUrl: config.baseUrl || DEFAULT_API_URL, - }; - - if (!this.config.publishableKey) { - throw new Error('[ZoriHQ] publishableKey is required'); - } - } - - /** - * Generate a unique visitor ID - */ - private generateVisitorId(): string { - return ( - 'vis_' + - 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { - const r = (Math.random() * 16) | 0; - const v = c === 'x' ? r : (r & 0x3) | 0x8; - return v.toString(16); - }) - ); - } - - /** - * Generate a unique session ID - */ - private generateSessionId(): string { - return ( - 'ses_' + - Date.now().toString(36) + - '_' + - Math.random().toString(36).substring(2, 9) - ); - } - - /** - * Generate a unique event ID - */ - private generateEventId(): string { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { - const r = (Math.random() * 16) | 0; - const v = c === 'x' ? r : (r & 0x3) | 0x8; - return v.toString(16); - }); - } - - /** - * Get or create visitor ID from cookies - */ - async getOrCreateVisitorId(): Promise { - const cookieStore = await cookies(); - let visitorId = cookieStore.get(COOKIE_NAME)?.value; - - if (!visitorId) { - visitorId = this.generateVisitorId(); - cookieStore.set(COOKIE_NAME, visitorId, { - maxAge: COOKIE_EXPIRY_DAYS * 24 * 60 * 60, - path: '/', - sameSite: 'lax', - }); - } - - return visitorId; - } - - /** - * Get or create session ID from cookies - */ - async getOrCreateSessionId(): Promise { - const cookieStore = await cookies(); - let sessionId = cookieStore.get(SESSION_COOKIE_NAME)?.value; - - if (!sessionId) { - sessionId = this.generateSessionId(); - cookieStore.set(SESSION_COOKIE_NAME, sessionId, { - path: '/', - sameSite: 'lax', - }); - } - - return sessionId; - } - - /** - * Get request metadata from Next.js headers - */ - private async getRequestMetadata() { - const headersList = await headers(); - return { - userAgent: headersList.get('user-agent') || 'unknown', - referrer: headersList.get('referer') || null, - host: headersList.get('host') || 'unknown', - }; - } - - /** - * Extract UTM parameters from URL - */ - private getUTMParameters(url: string): Record | null { - try { - const urlObj = new URL(url); - const utmParams: Record = {}; - - ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'].forEach((param) => { - const value = urlObj.searchParams.get(param); - if (value) { - utmParams[param] = value; - } - }); - - return Object.keys(utmParams).length > 0 ? utmParams : null; - } catch { - return null; - } - } - - /** - * Send event to ingestion endpoint - */ - private async sendEvent(eventData: any, endpoint: string = '/ingest'): Promise { - try { - const baseUrl = this.config.baseUrl.replace(/\/ingest$/, ''); - const url = baseUrl + endpoint; - - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Zori-PT': this.config.publishableKey, - 'X-Zori-Version': VERSION, - }, - body: JSON.stringify(eventData), - }); - - if (!response.ok) { - console.warn(`[ZoriHQ] Failed to send event to ${endpoint}:`, response.status); - return false; - } - - return true; - } catch (error) { - console.error(`[ZoriHQ] Error sending event to ${endpoint}:`, error); - return false; - } - } - - /** - * Track a custom event - */ - async track(options: TrackEventOptions): Promise { - const metadata = await this.getRequestMetadata(); - const visitorId = options.visitorId || (await this.getOrCreateVisitorId()); - const sessionId = options.sessionId || (await this.getOrCreateSessionId()); - - const eventData = { - event_name: options.eventName, - client_generated_event_id: this.generateEventId(), - visitor_id: visitorId, - session_id: sessionId, - client_timestamp_utc: new Date().toISOString(), - user_agent: options.userAgent || metadata.userAgent, - referrer: options.referrer || metadata.referrer, - page_url: options.pageUrl || '/', - host: options.host || metadata.host, - utm_parameters: options.pageUrl ? this.getUTMParameters(options.pageUrl) : null, - }; - - if (options.properties && Object.keys(options.properties).length > 0) { - (eventData as any).custom_properties = options.properties; - } - - return await this.sendEvent(eventData); - } - - /** - * Identify a user - */ - async identify(options: IdentifyOptions): Promise { - const metadata = await this.getRequestMetadata(); - const visitorId = options.visitorId || (await this.getOrCreateVisitorId()); - const sessionId = options.sessionId || (await this.getOrCreateSessionId()); - - const identifyData: any = { - visitor_id: visitorId, - session_id: sessionId, - client_timestamp_utc: new Date().toISOString(), - user_agent: options.userAgent || metadata.userAgent, - page_url: options.pageUrl || '/', - host: options.host || metadata.host, - }; - - if (options.userInfo.app_id) { - identifyData.app_id = options.userInfo.app_id; - } - - if (options.userInfo.email) { - identifyData.email = options.userInfo.email; - } - - if (options.userInfo.fullname || options.userInfo.full_name) { - identifyData.fullname = options.userInfo.fullname || options.userInfo.full_name; - } - - const additionalProps = { ...options.userInfo }; - delete additionalProps.app_id; - delete additionalProps.email; - delete additionalProps.fullname; - delete additionalProps.full_name; - - if (Object.keys(additionalProps).length > 0) { - identifyData.additional_properties = additionalProps; - } - - return await this.sendEvent(identifyData, '/identify'); - } - - /** - * Get the current visitor ID - */ - async getVisitorId(): Promise { - const cookieStore = await cookies(); - return cookieStore.get(COOKIE_NAME)?.value || null; - } - - /** - * Get the current session ID - */ - async getSessionId(): Promise { - const cookieStore = await cookies(); - return cookieStore.get(SESSION_COOKIE_NAME)?.value || null; - } -} - -// Export a factory function for easier usage -export function createZoriServer(config: ZoriConfig): ZoriServer { - return new ZoriServer(config); -} - -export default ZoriServer; diff --git a/nextjs-server/package.json b/nextjs-server/package.json deleted file mode 100644 index 89ab7a9..0000000 --- a/nextjs-server/package.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "name": "@zorihq/nextjs-server", - "version": "1.0.0", - "description": "ZoriHQ Analytics for Next.js (Server-side)", - "main": "index.ts", - "types": "index.ts", - "exports": { - ".": { - "import": "./index.ts", - "require": "./index.ts", - "types": "./index.ts" - } - }, - "files": [ - "index.ts", - "README.md", - "package.json" - ], - "scripts": { - "build": "tsc", - "prepublishOnly": "echo 'Publishing source files'", - "test": "echo 'Tests not yet configured'" - }, - "keywords": [ - "analytics", - "nextjs", - "next", - "server", - "tracking", - "zorihq" - ], - "author": "ZoriHQ", - "license": "MIT", - "peerDependencies": { - "next": ">=13.0.0" - }, - "devDependencies": { - "next": "^14.0.0", - "typescript": "^5.0.0" - }, - "repository": { - "type": "git", - "url": "https://github.com/zorihq/script.git", - "directory": "nextjs-server" - } -} diff --git a/nextjs-server/tsconfig.json b/nextjs-server/tsconfig.json deleted file mode 100644 index 4503eb5..0000000 --- a/nextjs-server/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "ESNext", - "lib": ["ES2020"], - "declaration": true, - "declarationMap": true, - "outDir": "./dist", - "rootDir": "./", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "moduleResolution": "node", - "resolveJsonModule": true, - "allowSyntheticDefaultImports": true - }, - "include": ["index.ts"], - "exclude": ["node_modules", "dist"] -} diff --git a/nextjs/index.tsx b/nextjs/index.tsx index faf7b59..3528494 100644 --- a/nextjs/index.tsx +++ b/nextjs/index.tsx @@ -2,37 +2,14 @@ import React, { createContext, useContext, useEffect, useRef, useState, useCallback } from 'react'; import { usePathname, useSearchParams } from 'next/navigation'; +import type { ZoriConfig, ConsentPreferences, UserInfo, ZoriCoreAPI } from '@zorihq/types'; -// Types -export interface ZoriConfig { - publishableKey: string; - baseUrl?: string; - comebackThreshold?: number; - trackQuickSwitches?: boolean; -} - -export interface ConsentPreferences { - analytics?: boolean; - marketing?: boolean; -} - -export interface UserInfo { - app_id?: string; - email?: string; - fullname?: string; - full_name?: string; - [key: string]: any; -} +// Re-export shared types for convenience +export type { ZoriConfig, ConsentPreferences, UserInfo } from '@zorihq/types'; -export interface ZoriContextType { +// Next.js-specific context type extending core API +export interface ZoriContextType extends ZoriCoreAPI { isInitialized: boolean; - track: (eventName: string, properties?: Record) => Promise; - identify: (userInfo: UserInfo) => Promise; - getVisitorId: () => Promise; - getSessionId: () => string | null; - setConsent: (preferences: ConsentPreferences) => boolean; - optOut: () => boolean; - hasConsent: () => boolean; } // Context diff --git a/nextjs/package.json b/nextjs/package.json index dff7850..349c17a 100644 --- a/nextjs/package.json +++ b/nextjs/package.json @@ -34,6 +34,9 @@ "next": ">=13.0.0", "react": ">=18.0.0" }, + "dependencies": { + "@zorihq/types": "^1.0.0" + }, "devDependencies": { "@types/react": "^18.0.0", "next": "^14.0.0", diff --git a/nextjs/pnpm-lock.yaml b/nextjs/pnpm-lock.yaml new file mode 100644 index 0000000..3a7a328 --- /dev/null +++ b/nextjs/pnpm-lock.yaml @@ -0,0 +1,321 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@zorihq/types': + specifier: ^1.0.0 + version: 1.0.0 + devDependencies: + '@types/react': + specifier: ^18.0.0 + version: 18.3.27 + next: + specifier: ^14.0.0 + version: 14.2.33(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: + specifier: ^18.0.0 + version: 18.3.1 + typescript: + specifier: ^5.0.0 + version: 5.9.3 + +packages: + + '@next/env@14.2.33': + resolution: {integrity: sha512-CgVHNZ1fRIlxkLhIX22flAZI/HmpDaZ8vwyJ/B0SDPTBuLZ1PJ+DWMjCHhqnExfmSQzA/PbZi8OAc7PAq2w9IA==} + + '@next/swc-darwin-arm64@14.2.33': + resolution: {integrity: sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@14.2.33': + resolution: {integrity: sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@14.2.33': + resolution: {integrity: sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-musl@14.2.33': + resolution: {integrity: sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-x64-gnu@14.2.33': + resolution: {integrity: sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-musl@14.2.33': + resolution: {integrity: sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-win32-arm64-msvc@14.2.33': + resolution: {integrity: sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-ia32-msvc@14.2.33': + resolution: {integrity: sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@next/swc-win32-x64-msvc@14.2.33': + resolution: {integrity: sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/helpers@0.5.5': + resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react@18.3.27': + resolution: {integrity: sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==} + + '@zorihq/types@1.0.0': + resolution: {integrity: sha512-5XcYod8Pbz34iAkIWBxnFop4djdzj8z5JDj47uOh3Xsj453ok7lpvl2DjcQOcVoaWRn6F69GjM6jQBzulOxHDg==} + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + + caniuse-lite@1.0.30001756: + resolution: {integrity: sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A==} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + next@14.2.33: + resolution: {integrity: sha512-GiKHLsD00t4ACm1p00VgrI0rUFAC9cRDGReKyERlM57aeEZkOQGcZTpIbsGn0b562FTPJWmYfKwplfO9EaT6ng==} + engines: {node: '>=18.17.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.41.2 + react: ^18.2.0 + react-dom: ^18.2.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + sass: + optional: true + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + + styled-jsx@5.1.1: + resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + +snapshots: + + '@next/env@14.2.33': {} + + '@next/swc-darwin-arm64@14.2.33': + optional: true + + '@next/swc-darwin-x64@14.2.33': + optional: true + + '@next/swc-linux-arm64-gnu@14.2.33': + optional: true + + '@next/swc-linux-arm64-musl@14.2.33': + optional: true + + '@next/swc-linux-x64-gnu@14.2.33': + optional: true + + '@next/swc-linux-x64-musl@14.2.33': + optional: true + + '@next/swc-win32-arm64-msvc@14.2.33': + optional: true + + '@next/swc-win32-ia32-msvc@14.2.33': + optional: true + + '@next/swc-win32-x64-msvc@14.2.33': + optional: true + + '@swc/counter@0.1.3': {} + + '@swc/helpers@0.5.5': + dependencies: + '@swc/counter': 0.1.3 + tslib: 2.8.1 + + '@types/prop-types@15.7.15': {} + + '@types/react@18.3.27': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + + '@zorihq/types@1.0.0': {} + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + + caniuse-lite@1.0.30001756: {} + + client-only@0.0.1: {} + + csstype@3.2.3: {} + + graceful-fs@4.2.11: {} + + js-tokens@4.0.0: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + nanoid@3.3.11: {} + + next@14.2.33(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@next/env': 14.2.33 + '@swc/helpers': 0.5.5 + busboy: 1.6.0 + caniuse-lite: 1.0.30001756 + graceful-fs: 4.2.11 + postcss: 8.4.31 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + styled-jsx: 5.1.1(react@18.3.1) + optionalDependencies: + '@next/swc-darwin-arm64': 14.2.33 + '@next/swc-darwin-x64': 14.2.33 + '@next/swc-linux-arm64-gnu': 14.2.33 + '@next/swc-linux-arm64-musl': 14.2.33 + '@next/swc-linux-x64-gnu': 14.2.33 + '@next/swc-linux-x64-musl': 14.2.33 + '@next/swc-win32-arm64-msvc': 14.2.33 + '@next/swc-win32-ia32-msvc': 14.2.33 + '@next/swc-win32-x64-msvc': 14.2.33 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + picocolors@1.1.1: {} + + postcss@8.4.31: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + source-map-js@1.2.1: {} + + streamsearch@1.1.0: {} + + styled-jsx@5.1.1(react@18.3.1): + dependencies: + client-only: 0.0.1 + react: 18.3.1 + + tslib@2.8.1: {} + + typescript@5.9.3: {} diff --git a/react/index.tsx b/react/index.tsx index 6f2bf1e..960cc32 100644 --- a/react/index.tsx +++ b/react/index.tsx @@ -1,35 +1,12 @@ import React, { createContext, useContext, useEffect, useRef, useState, useCallback } from 'react'; +import type { ZoriConfig, ConsentPreferences, UserInfo, ZoriCoreAPI } from '@zorihq/types'; -// Types -export interface ZoriConfig { - publishableKey: string; - baseUrl?: string; - comebackThreshold?: number; - trackQuickSwitches?: boolean; -} - -export interface ConsentPreferences { - analytics?: boolean; - marketing?: boolean; -} - -export interface UserInfo { - app_id?: string; - email?: string; - fullname?: string; - full_name?: string; - [key: string]: any; -} +// Re-export shared types for convenience +export type { ZoriConfig, ConsentPreferences, UserInfo } from '@zorihq/types'; -export interface ZoriContextType { +// React-specific context type extending core API +export interface ZoriContextType extends ZoriCoreAPI { isInitialized: boolean; - track: (eventName: string, properties?: Record) => Promise; - identify: (userInfo: UserInfo) => Promise; - getVisitorId: () => Promise; - getSessionId: () => string | null; - setConsent: (preferences: ConsentPreferences) => boolean; - optOut: () => boolean; - hasConsent: () => boolean; } // Context diff --git a/react/package.json b/react/package.json index 19e4063..eb62969 100644 --- a/react/package.json +++ b/react/package.json @@ -32,6 +32,9 @@ "peerDependencies": { "react": ">=16.8.0" }, + "dependencies": { + "@zorihq/types": "^1.0.0" + }, "devDependencies": { "@types/react": "^18.0.0", "react": "^18.0.0", diff --git a/react/pnpm-lock.yaml b/react/pnpm-lock.yaml new file mode 100644 index 0000000..15f49cc --- /dev/null +++ b/react/pnpm-lock.yaml @@ -0,0 +1,78 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@zorihq/types': + specifier: ^1.0.0 + version: 1.0.0 + devDependencies: + '@types/react': + specifier: ^18.0.0 + version: 18.3.27 + react: + specifier: ^18.0.0 + version: 18.3.1 + typescript: + specifier: ^5.0.0 + version: 5.9.3 + +packages: + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react@18.3.27': + resolution: {integrity: sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==} + + '@zorihq/types@1.0.0': + resolution: {integrity: sha512-5XcYod8Pbz34iAkIWBxnFop4djdzj8z5JDj47uOh3Xsj453ok7lpvl2DjcQOcVoaWRn6F69GjM6jQBzulOxHDg==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + +snapshots: + + '@types/prop-types@15.7.15': {} + + '@types/react@18.3.27': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + + '@zorihq/types@1.0.0': {} + + csstype@3.2.3: {} + + js-tokens@4.0.0: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + typescript@5.9.3: {} diff --git a/shared/package.json b/shared/package.json new file mode 100644 index 0000000..42f6b0c --- /dev/null +++ b/shared/package.json @@ -0,0 +1,34 @@ +{ + "name": "@zorihq/types", + "version": "1.0.0", + "description": "Shared TypeScript types for ZoriHQ Analytics SDKs", + "main": "types.ts", + "types": "types.ts", + "exports": { + ".": { + "import": "./types.ts", + "require": "./types.ts", + "types": "./types.ts" + } + }, + "files": [ + "types.ts", + "package.json" + ], + "scripts": { + "prepublishOnly": "echo 'Publishing source files'" + }, + "keywords": [ + "analytics", + "types", + "typescript", + "zorihq" + ], + "author": "ZoriHQ", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/zorihq/script.git", + "directory": "shared" + } +} diff --git a/shared/types.ts b/shared/types.ts new file mode 100644 index 0000000..f640cd0 --- /dev/null +++ b/shared/types.ts @@ -0,0 +1,132 @@ +/** + * Shared TypeScript types for ZoriHQ SDKs + * These types are reused across React, Next.js, Vue, Svelte, and server-side implementations + */ + +// ============================================================================= +// Core Configuration Types +// ============================================================================= + +/** + * Configuration options for ZoriHQ client-side SDKs + */ +export interface ZoriConfig { + /** Your ZoriHQ publishable key */ + publishableKey: string; + /** Custom API base URL (optional) */ + baseUrl?: string; + /** Threshold in ms for detecting user comebacks (optional) */ + comebackThreshold?: number; + /** Whether to track quick tab switches (optional) */ + trackQuickSwitches?: boolean; +} + +/** + * Configuration options for ZoriHQ server-side SDK + * A subset of ZoriConfig without client-specific options + */ +export interface ZoriServerConfig { + /** Your ZoriHQ publishable key */ + publishableKey: string; + /** Custom API base URL (optional) */ + baseUrl?: string; +} + +// ============================================================================= +// User & Consent Types +// ============================================================================= + +/** + * User consent preferences for tracking + */ +export interface ConsentPreferences { + /** Whether analytics tracking is allowed */ + analytics?: boolean; + /** Whether marketing tracking is allowed */ + marketing?: boolean; +} + +/** + * User identification information + */ +export interface UserInfo { + /** Application-specific user ID */ + app_id?: string; + /** User's email address */ + email?: string; + /** User's full name (alternative 1) */ + fullname?: string; + /** User's full name (alternative 2) */ + full_name?: string; + /** Additional custom properties */ + [key: string]: any; +} + +// ============================================================================= +// Core API Interface +// ============================================================================= + +/** + * Core API methods shared across all ZoriHQ client SDKs + * Framework-specific implementations extend or implement this interface + */ +export interface ZoriCoreAPI { + /** Track a custom event */ + track: (eventName: string, properties?: Record) => Promise; + /** Identify a user */ + identify: (userInfo: UserInfo) => Promise; + /** Get the current visitor ID */ + getVisitorId: () => Promise; + /** Get the current session ID */ + getSessionId: () => string | null; + /** Set user consent preferences */ + setConsent: (preferences: ConsentPreferences) => boolean; + /** Opt out of all tracking */ + optOut: () => boolean; + /** Check if user has given consent */ + hasConsent: () => boolean; +} + +// ============================================================================= +// Server-Side Types +// ============================================================================= + +/** + * Options for tracking events on the server side + */ +export interface TrackEventOptions { + /** Name of the event to track */ + eventName: string; + /** Custom properties to attach to the event */ + properties?: Record; + /** Override visitor ID (optional, auto-generated if not provided) */ + visitorId?: string; + /** Override session ID (optional, auto-generated if not provided) */ + sessionId?: string; + /** User agent string */ + userAgent?: string; + /** Full page URL */ + pageUrl?: string; + /** Host/domain name */ + host?: string; + /** Referrer URL */ + referrer?: string; +} + +/** + * Options for identifying users on the server side + */ +export interface IdentifyOptions { + /** User information to associate with the visitor */ + userInfo: UserInfo; + /** Override visitor ID (optional, auto-generated if not provided) */ + visitorId?: string; + /** Override session ID (optional, auto-generated if not provided) */ + sessionId?: string; + /** User agent string */ + userAgent?: string; + /** Full page URL */ + pageUrl?: string; + /** Host/domain name */ + host?: string; +} diff --git a/vue/index.ts b/vue/index.ts index bb23e15..91a0d54 100644 --- a/vue/index.ts +++ b/vue/index.ts @@ -9,77 +9,59 @@ import { type App, type Ref, type InjectionKey, -} from 'vue'; - -// Types -export interface ZoriConfig { - publishableKey: string; - baseUrl?: string; - comebackThreshold?: number; - trackQuickSwitches?: boolean; -} - -export interface ConsentPreferences { - analytics?: boolean; - marketing?: boolean; -} +} from "vue"; +import type { + ZoriConfig, + ConsentPreferences, + UserInfo, + ZoriCoreAPI, +} from "@zorihq/types"; -export interface UserInfo { - app_id?: string; - email?: string; - fullname?: string; - full_name?: string; - [key: string]: any; -} +export type { ZoriConfig, ConsentPreferences, UserInfo } from "@zorihq/types"; -export interface ZoriInstance { +export interface ZoriInstance extends Omit { isInitialized: Readonly>; - track: (eventName: string, properties?: Record) => Promise; - identify: (userInfo: UserInfo) => Promise; - getVisitorId: () => Promise; getSessionId: () => string | null; - setConsent: (preferences: ConsentPreferences) => boolean; - optOut: () => boolean; - hasConsent: () => boolean; } -// Injection Key -export const ZoriKey: InjectionKey = Symbol('zori'); +export const ZoriKey: InjectionKey = Symbol("zori"); -// Plugin Options export interface ZoriPluginOptions { config: ZoriConfig; - router?: any; // Vue Router instance + router?: any; autoTrackPageViews?: boolean; } -// Create Zori Instance function createZoriInstance(config: ZoriConfig): ZoriInstance { const isInitialized = ref(false); let scriptLoaded = false; const loadScript = () => { - if (scriptLoaded || typeof window === 'undefined') return; + if (scriptLoaded || typeof window === "undefined") return; - // Initialize queue (window as any).ZoriHQ = (window as any).ZoriHQ || []; - // Load script - const script = document.createElement('script'); - script.src = 'https://cdn.zorihq.com/script.min.js'; + const script = document.createElement("script"); + script.src = "https://cdn.zorihq.com/script.min.js"; script.async = true; - script.setAttribute('data-key', config.publishableKey); + script.setAttribute("data-key", config.publishableKey); if (config.baseUrl) { - script.setAttribute('data-base-url', config.baseUrl); + script.setAttribute("data-base-url", config.baseUrl); } if (config.comebackThreshold !== undefined) { - script.setAttribute('data-comeback-threshold', config.comebackThreshold.toString()); + script.setAttribute( + "data-comeback-threshold", + config.comebackThreshold.toString(), + ); } if (config.trackQuickSwitches !== undefined) { - script.setAttribute('data-track-quick-switches', config.trackQuickSwitches.toString()); + script.setAttribute( + "data-track-quick-switches", + config.trackQuickSwitches.toString(), + ); } script.onload = () => { @@ -90,14 +72,17 @@ function createZoriInstance(config: ZoriConfig): ZoriInstance { scriptLoaded = true; }; - const track = async (eventName: string, properties?: Record): Promise => { + const track = async ( + eventName: string, + properties?: Record, + ): Promise => { const zori = (window as any).ZoriHQ; if (!zori) return false; - if (typeof zori.track === 'function') { + if (typeof zori.track === "function") { return await zori.track(eventName, properties); } else { - zori.push(['track', eventName, properties]); + zori.push(["track", eventName, properties]); return true; } }; @@ -106,30 +91,30 @@ function createZoriInstance(config: ZoriConfig): ZoriInstance { const zori = (window as any).ZoriHQ; if (!zori) return false; - if (typeof zori.identify === 'function') { + if (typeof zori.identify === "function") { return await zori.identify(userInfo); } else { - zori.push(['identify', userInfo]); + zori.push(["identify", userInfo]); return true; } }; const getVisitorId = async (): Promise => { const zori = (window as any).ZoriHQ; - if (!zori) return ''; + if (!zori) return ""; - if (typeof zori.getVisitorId === 'function') { + if (typeof zori.getVisitorId === "function") { return await zori.getVisitorId(); } return new Promise((resolve) => { - zori.push(['getVisitorId', (id: string) => resolve(id)]); + zori.push(["getVisitorId", (id: string) => resolve(id)]); }); }; const getSessionId = (): string | null => { const zori = (window as any).ZoriHQ; - if (!zori || typeof zori.getSessionId !== 'function') return null; + if (!zori || typeof zori.getSessionId !== "function") return null; return zori.getSessionId(); }; @@ -137,10 +122,10 @@ function createZoriInstance(config: ZoriConfig): ZoriInstance { const zori = (window as any).ZoriHQ; if (!zori) return false; - if (typeof zori.setConsent === 'function') { + if (typeof zori.setConsent === "function") { return zori.setConsent(preferences); } else { - zori.push(['setConsent', preferences]); + zori.push(["setConsent", preferences]); return true; } }; @@ -149,22 +134,21 @@ function createZoriInstance(config: ZoriConfig): ZoriInstance { const zori = (window as any).ZoriHQ; if (!zori) return false; - if (typeof zori.optOut === 'function') { + if (typeof zori.optOut === "function") { return zori.optOut(); } else { - zori.push(['optOut']); + zori.push(["optOut"]); return true; } }; const hasConsent = (): boolean => { const zori = (window as any).ZoriHQ; - if (!zori || typeof zori.hasConsent !== 'function') return true; + if (!zori || typeof zori.hasConsent !== "function") return true; return zori.hasConsent(); }; - // Load script immediately - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { loadScript(); } @@ -180,21 +164,19 @@ function createZoriInstance(config: ZoriConfig): ZoriInstance { }; } -// Vue Plugin export const ZoriPlugin = { install(app: App, options: ZoriPluginOptions) { const zoriInstance = createZoriInstance(options.config); app.provide(ZoriKey, zoriInstance); - // Auto-track page views with Vue Router if (options.router && options.autoTrackPageViews !== false) { options.router.afterEach((to: any) => { if (zoriInstance.isInitialized.value) { - zoriInstance.track('page_view', { + zoriInstance.track("page_view", { page_title: document.title, page_path: to.path, page_name: to.name, - page_search: to.query ? JSON.stringify(to.query) : '', + page_search: to.query ? JSON.stringify(to.query) : "", }); } }); @@ -202,23 +184,28 @@ export const ZoriPlugin = { }, }; -// Composable: useZori export function useZori(): ZoriInstance { const zori = inject(ZoriKey); if (!zori) { - throw new Error('useZori must be used within a component with ZoriPlugin installed'); + throw new Error( + "useZori must be used within a component with ZoriPlugin installed", + ); } return zori; } -// Composable: usePageView -export function usePageView(properties?: Ref> | Record) { +export function usePageView( + properties?: Ref> | Record, +) { const { track, isInitialized } = useZori(); onMounted(() => { if (isInitialized.value) { - const props = typeof properties === 'object' && 'value' in properties ? properties.value : properties; - track('page_view', { + const props = + typeof properties === "object" && "value" in properties + ? properties.value + : properties; + track("page_view", { page_title: document.title, page_path: window.location.pathname, page_search: window.location.search, @@ -228,13 +215,12 @@ export function usePageView(properties?: Ref> | Record { if (isInitialized.value) { - track('page_view', { + track("page_view", { page_title: document.title, page_path: window.location.pathname, page_search: window.location.search, @@ -243,54 +229,58 @@ export function usePageView(properties?: Ref> | Record, - properties?: Ref> | Record + properties?: Ref> | Record, ) { const { track, isInitialized } = useZori(); onMounted(() => { if (isInitialized.value) { - const name = typeof eventName === 'string' ? eventName : eventName.value; - const props = typeof properties === 'object' && 'value' in properties ? properties.value : properties; + const name = typeof eventName === "string" ? eventName : eventName.value; + const props = + typeof properties === "object" && "value" in properties + ? properties.value + : properties; track(name, props); } }); - // Watch for changes watch( [ - typeof eventName === 'string' ? ref(eventName) : eventName, - typeof properties === 'object' && 'value' in properties ? properties : ref(properties), + typeof eventName === "string" ? ref(eventName) : eventName, + typeof properties === "object" && "value" in properties + ? properties + : ref(properties), ], ([newName, newProps]) => { if (isInitialized.value) { track(newName as string, newProps as Record); } }, - { deep: true } + { deep: true }, ); } -// Composable: useIdentify -export function useIdentify(userInfo: Ref | UserInfo | null) { +export function useIdentify(userInfo: Ref | UserInfo) { const { identify, isInitialized } = useZori(); onMounted(() => { - const info = typeof userInfo === 'object' && 'value' in userInfo ? userInfo.value : userInfo; + const info = + typeof userInfo === "object" && "value" in userInfo + ? userInfo.value + : userInfo; if (isInitialized.value && info) { identify(info); } }); - // Watch for changes if userInfo is a ref - if (userInfo && typeof userInfo === 'object' && 'value' in userInfo) { + if (userInfo && typeof userInfo === "object" && "value" in userInfo) { watch( userInfo, (newInfo) => { @@ -298,12 +288,11 @@ export function useIdentify(userInfo: Ref | UserInfo | null) { identify(newInfo); } }, - { deep: true } + { deep: true }, ); } } -// Export default export default { ZoriPlugin, useZori, diff --git a/vue/package.json b/vue/package.json index 8178240..421401c 100644 --- a/vue/package.json +++ b/vue/package.json @@ -33,6 +33,9 @@ "peerDependencies": { "vue": ">=3.0.0" }, + "dependencies": { + "@zorihq/types": "^1.0.0" + }, "devDependencies": { "typescript": "^5.0.0", "vue": "^3.0.0" diff --git a/vue/pnpm-lock.yaml b/vue/pnpm-lock.yaml new file mode 100644 index 0000000..2a620df --- /dev/null +++ b/vue/pnpm-lock.yaml @@ -0,0 +1,223 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@zorihq/types': + specifier: ^1.0.0 + version: 1.0.0 + devDependencies: + typescript: + specifier: ^5.0.0 + version: 5.9.3 + vue: + specifier: ^3.0.0 + version: 3.5.24(typescript@5.9.3) + +packages: + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + engines: {node: '>=6.9.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@vue/compiler-core@3.5.24': + resolution: {integrity: sha512-eDl5H57AOpNakGNAkFDH+y7kTqrQpJkZFXhWZQGyx/5Wh7B1uQYvcWkvZi11BDhscPgj8N7XV3oRwiPnx1Vrig==} + + '@vue/compiler-dom@3.5.24': + resolution: {integrity: sha512-1QHGAvs53gXkWdd3ZMGYuvQFXHW4ksKWPG8HP8/2BscrbZ0brw183q2oNWjMrSWImYLHxHrx1ItBQr50I/q2zw==} + + '@vue/compiler-sfc@3.5.24': + resolution: {integrity: sha512-8EG5YPRgmTB+YxYBM3VXy8zHD9SWHUJLIGPhDovo3Z8VOgvP+O7UP5vl0J4BBPWYD9vxtBabzW1EuEZ+Cqs14g==} + + '@vue/compiler-ssr@3.5.24': + resolution: {integrity: sha512-trOvMWNBMQ/odMRHW7Ae1CdfYx+7MuiQu62Jtu36gMLXcaoqKvAyh+P73sYG9ll+6jLB6QPovqoKGGZROzkFFg==} + + '@vue/reactivity@3.5.24': + resolution: {integrity: sha512-BM8kBhtlkkbnyl4q+HiF5R5BL0ycDPfihowulm02q3WYp2vxgPcJuZO866qa/0u3idbMntKEtVNuAUp5bw4teg==} + + '@vue/runtime-core@3.5.24': + resolution: {integrity: sha512-RYP/byyKDgNIqfX/gNb2PB55dJmM97jc9wyF3jK7QUInYKypK2exmZMNwnjueWwGceEkP6NChd3D2ZVEp9undQ==} + + '@vue/runtime-dom@3.5.24': + resolution: {integrity: sha512-Z8ANhr/i0XIluonHVjbUkjvn+CyrxbXRIxR7wn7+X7xlcb7dJsfITZbkVOeJZdP8VZwfrWRsWdShH6pngMxRjw==} + + '@vue/server-renderer@3.5.24': + resolution: {integrity: sha512-Yh2j2Y4G/0/4z/xJ1Bad4mxaAk++C2v4kaa8oSYTMJBJ00/ndPuxCnWeot0/7/qafQFLh5pr6xeV6SdMcE/G1w==} + peerDependencies: + vue: 3.5.24 + + '@vue/shared@3.5.24': + resolution: {integrity: sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A==} + + '@zorihq/types@1.0.0': + resolution: {integrity: sha512-5XcYod8Pbz34iAkIWBxnFop4djdzj8z5JDj47uOh3Xsj453ok7lpvl2DjcQOcVoaWRn6F69GjM6jQBzulOxHDg==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + vue@3.5.24: + resolution: {integrity: sha512-uTHDOpVQTMjcGgrqFPSb8iO2m1DUvo+WbGqoXQz8Y1CeBYQ0FXf2z1gLRaBtHjlRz7zZUBHxjVB5VTLzYkvftg==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + +snapshots: + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.28.5': + dependencies: + '@babel/types': 7.28.5 + + '@babel/types@7.28.5': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@vue/compiler-core@3.5.24': + dependencies: + '@babel/parser': 7.28.5 + '@vue/shared': 3.5.24 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.24': + dependencies: + '@vue/compiler-core': 3.5.24 + '@vue/shared': 3.5.24 + + '@vue/compiler-sfc@3.5.24': + dependencies: + '@babel/parser': 7.28.5 + '@vue/compiler-core': 3.5.24 + '@vue/compiler-dom': 3.5.24 + '@vue/compiler-ssr': 3.5.24 + '@vue/shared': 3.5.24 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.6 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.24': + dependencies: + '@vue/compiler-dom': 3.5.24 + '@vue/shared': 3.5.24 + + '@vue/reactivity@3.5.24': + dependencies: + '@vue/shared': 3.5.24 + + '@vue/runtime-core@3.5.24': + dependencies: + '@vue/reactivity': 3.5.24 + '@vue/shared': 3.5.24 + + '@vue/runtime-dom@3.5.24': + dependencies: + '@vue/reactivity': 3.5.24 + '@vue/runtime-core': 3.5.24 + '@vue/shared': 3.5.24 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.24(vue@3.5.24(typescript@5.9.3))': + dependencies: + '@vue/compiler-ssr': 3.5.24 + '@vue/shared': 3.5.24 + vue: 3.5.24(typescript@5.9.3) + + '@vue/shared@3.5.24': {} + + '@zorihq/types@1.0.0': {} + + csstype@3.2.3: {} + + entities@4.5.0: {} + + estree-walker@2.0.2: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + nanoid@3.3.11: {} + + picocolors@1.1.1: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + source-map-js@1.2.1: {} + + typescript@5.9.3: {} + + vue@3.5.24(typescript@5.9.3): + dependencies: + '@vue/compiler-dom': 3.5.24 + '@vue/compiler-sfc': 3.5.24 + '@vue/runtime-dom': 3.5.24 + '@vue/server-renderer': 3.5.24(vue@3.5.24(typescript@5.9.3)) + '@vue/shared': 3.5.24 + optionalDependencies: + typescript: 5.9.3