diff --git a/astro/README.md b/astro/README.md new file mode 100644 index 0000000..818cd7b --- /dev/null +++ b/astro/README.md @@ -0,0 +1,280 @@ +# @zorihq/astro + +ZoriHQ Analytics SDK for Astro applications. + +## Installation + +```bash +npm install @zorihq/astro +# or +pnpm add @zorihq/astro +# or +yarn add @zorihq/astro +``` + +## Quick Start + +### 1. Add the ZoriScript component + +Add the `ZoriScript` component to your layout or base template: + +```astro +--- +// src/layouts/Layout.astro +import { ZoriScript } from '@zorihq/astro/ZoriScript.astro'; +--- + + + + + + + + + +``` + +### 2. Track custom events + +Use the client-side utilities in your scripts: + +```astro +--- +// src/pages/index.astro +import Layout from '../layouts/Layout.astro'; +--- + + + + + + +``` + +## Configuration + +The `ZoriScript` component accepts the following props: + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `publishableKey` | `string` | **required** | Your ZoriHQ publishable key | +| `baseUrl` | `string` | `undefined` | Custom API base URL | +| `comebackThreshold` | `number` | `undefined` | Threshold in ms for detecting user comebacks | +| `trackQuickSwitches` | `boolean` | `undefined` | Whether to track quick tab switches | +| `autoTrackPageViews` | `boolean` | `true` | Whether to auto-track page views | + +### Example with all options + +```astro + +``` + +## View Transitions Support + +The SDK automatically supports Astro's View Transitions. Page views are tracked on: +- Initial page load +- Navigation via View Transitions (`astro:page-load` event) + +No additional configuration is needed. + +## Client-Side API + +### track(eventName, properties?) + +Track a custom event: + +```typescript +import { track } from '@zorihq/astro'; + +await track('button_click', { button_id: 'cta-primary' }); +``` + +### identify(userInfo) + +Identify a user: + +```typescript +import { identify } from '@zorihq/astro'; + +await identify({ + app_id: 'user-123', + email: 'user@example.com', + full_name: 'John Doe', + plan: 'premium' +}); +``` + +### trackPageView(properties?) + +Manually track a page view (useful when auto-tracking is disabled): + +```typescript +import { trackPageView } from '@zorihq/astro'; + +await trackPageView({ custom_property: 'value' }); +``` + +### getVisitorId() + +Get the current visitor ID: + +```typescript +import { getVisitorId } from '@zorihq/astro'; + +const visitorId = await getVisitorId(); +``` + +### getSessionId() + +Get the current session ID: + +```typescript +import { getSessionId } from '@zorihq/astro'; + +const sessionId = getSessionId(); +``` + +### setConsent(preferences) + +Set user consent preferences: + +```typescript +import { setConsent } from '@zorihq/astro'; + +setConsent({ + analytics: true, + marketing: false +}); +``` + +### optOut() + +Opt the user out of all tracking: + +```typescript +import { optOut } from '@zorihq/astro'; + +optOut(); +``` + +### hasConsent() + +Check if the user has given consent: + +```typescript +import { hasConsent } from '@zorihq/astro'; + +if (hasConsent()) { + // User has given consent +} +``` + +### isInitialized() + +Check if ZoriHQ is initialized: + +```typescript +import { isInitialized } from '@zorihq/astro'; + +if (isInitialized()) { + // Script is fully loaded +} +``` + +### waitForInit(timeout?) + +Wait for ZoriHQ to be initialized: + +```typescript +import { waitForInit, track } from '@zorihq/astro'; + +await waitForInit(5000); // Wait up to 5 seconds +await track('initialized'); +``` + +### createClickHandler(eventName?, properties?) + +Create a reusable click handler: + +```typescript +import { createClickHandler } from '@zorihq/astro'; + +const handler = createClickHandler('cta_click', { location: 'header' }); +document.getElementById('cta')?.addEventListener('click', handler); +``` + +## Using with Framework Components + +If you're using React, Vue, or Svelte components in your Astro project, you can use the client-side utilities within those components: + +### React Component Example + +```tsx +// src/components/SignupButton.tsx +import { track } from '@zorihq/astro'; + +export default function SignupButton() { + const handleClick = () => { + track('signup_clicked', { component: 'react' }); + }; + + return ; +} +``` + +### Vue Component Example + +```vue + + + + +``` + +## TypeScript Support + +This package includes TypeScript definitions. All types are exported: + +```typescript +import type { ZoriConfig, ConsentPreferences, UserInfo } from '@zorihq/astro'; + +const config: ZoriConfig = { + publishableKey: 'your-key', + baseUrl: 'https://api.example.com' +}; + +const user: UserInfo = { + app_id: '123', + email: 'user@example.com' +}; +``` + +## SSR Considerations + +The client-side utilities (`track`, `identify`, etc.) are designed to run in the browser. When using them in SSR contexts, they will safely return without effect. + +For server-side tracking needs, consider using the tracking API directly from your server endpoints. + +## License + +MIT diff --git a/astro/ZoriScript.astro b/astro/ZoriScript.astro new file mode 100644 index 0000000..b86519d --- /dev/null +++ b/astro/ZoriScript.astro @@ -0,0 +1,87 @@ +--- +/** + * ZoriScript - Astro component for ZoriHQ Analytics + * + * This component injects the ZoriHQ tracking script and optionally + * tracks page views automatically, including support for View Transitions. + */ + +export interface Props { + /** 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; + /** Whether to auto-track page views (default: true) */ + autoTrackPageViews?: boolean; +} + +const { + publishableKey, + baseUrl, + comebackThreshold, + trackQuickSwitches, + autoTrackPageViews = true, +} = Astro.props; +--- + + + +{autoTrackPageViews && ( + +)} diff --git a/astro/index.ts b/astro/index.ts new file mode 100644 index 0000000..7920ecb --- /dev/null +++ b/astro/index.ts @@ -0,0 +1,212 @@ +/** + * ZoriHQ Analytics for Astro + * + * Client-side utilities for tracking events, identifying users, + * and managing analytics in Astro applications. + */ + +import type { ZoriConfig, ConsentPreferences, UserInfo, ZoriCoreAPI } from '@zorihq/types'; + +// Re-export shared types for convenience +export type { ZoriConfig, ConsentPreferences, UserInfo } from '@zorihq/types'; + +// Extend Window interface for ZoriHQ +declare global { + interface Window { + ZoriHQ: any; + __zoriInitialPageTracked?: boolean; + } +} + +/** + * Get the ZoriHQ instance from window + */ +function getZori(): any { + if (typeof window === 'undefined') return null; + return window.ZoriHQ; +} + +/** + * Track a custom event + * @param eventName - Name of the event to track + * @param properties - Optional properties to attach to the event + * @returns Promise resolving to true if tracking was successful + */ +export async function track(eventName: string, properties?: Record): Promise { + const zori = getZori(); + if (!zori) return false; + + if (typeof zori.track === 'function') { + return await zori.track(eventName, properties); + } else { + zori.push(['track', eventName, properties]); + return true; + } +} + +/** + * Identify a user with their information + * @param userInfo - User information to associate with the visitor + * @returns Promise resolving to true if identification was successful + */ +export async function identify(userInfo: UserInfo): Promise { + const zori = getZori(); + if (!zori) return false; + + if (typeof zori.identify === 'function') { + return await zori.identify(userInfo); + } else { + zori.push(['identify', userInfo]); + return true; + } +} + +/** + * Get the current visitor ID + * @returns Promise resolving to the visitor ID string + */ +export async function getVisitorId(): Promise { + const zori = getZori(); + if (!zori) return ''; + + if (typeof zori.getVisitorId === 'function') { + return await zori.getVisitorId(); + } + + return new Promise((resolve) => { + zori.push(['getVisitorId', (id: string) => resolve(id)]); + }); +} + +/** + * Get the current session ID + * @returns The session ID or null if not available + */ +export function getSessionId(): string | null { + const zori = getZori(); + if (!zori || typeof zori.getSessionId !== 'function') return null; + return zori.getSessionId(); +} + +/** + * Set user consent preferences + * @param preferences - Consent preferences object + * @returns true if consent was set successfully + */ +export function setConsent(preferences: ConsentPreferences): boolean { + const zori = getZori(); + if (!zori) return false; + + if (typeof zori.setConsent === 'function') { + return zori.setConsent(preferences); + } else { + zori.push(['setConsent', preferences]); + return true; + } +} + +/** + * Opt out of all tracking + * @returns true if opt-out was successful + */ +export function optOut(): boolean { + const zori = getZori(); + if (!zori) return false; + + if (typeof zori.optOut === 'function') { + return zori.optOut(); + } else { + zori.push(['optOut']); + return true; + } +} + +/** + * Check if user has given consent + * @returns true if user has given consent + */ +export function hasConsent(): boolean { + const zori = getZori(); + if (!zori || typeof zori.hasConsent !== 'function') return true; + return zori.hasConsent(); +} + +/** + * Check if ZoriHQ script is loaded and initialized + * @returns true if the script is fully initialized + */ +export function isInitialized(): boolean { + const zori = getZori(); + return zori && typeof zori.track === 'function'; +} + +/** + * Wait for ZoriHQ to be initialized + * @param timeout - Maximum time to wait in ms (default: 5000) + * @returns Promise that resolves when initialized or rejects on timeout + */ +export function waitForInit(timeout: number = 5000): Promise { + return new Promise((resolve, reject) => { + if (isInitialized()) { + resolve(); + return; + } + + const startTime = Date.now(); + const checkInterval = setInterval(() => { + if (isInitialized()) { + clearInterval(checkInterval); + resolve(); + } else if (Date.now() - startTime > timeout) { + clearInterval(checkInterval); + reject(new Error('ZoriHQ initialization timeout')); + } + }, 100); + }); +} + +/** + * Track a page view manually + * Useful when auto-tracking is disabled or for custom page view tracking + * @param properties - Optional additional properties + */ +export async function trackPageView(properties?: Record): Promise { + return track('page_view', { + page_title: typeof document !== 'undefined' ? document.title : '', + page_path: typeof window !== 'undefined' ? window.location.pathname : '', + page_search: typeof window !== 'undefined' ? window.location.search : '', + page_hash: typeof window !== 'undefined' ? window.location.hash : '', + ...properties, + }); +} + +/** + * Create a click tracking handler + * @param eventName - Name of the event to track (default: 'click') + * @param properties - Properties to attach to the event + * @returns Click handler function + */ +export function createClickHandler( + eventName: string = 'click', + properties?: Record +): (event: Event) => void { + return (event: Event) => { + track(eventName, properties); + }; +} + +/** + * ZoriHQ API object implementing ZoriCoreAPI interface + * Provides a unified API for all tracking operations + */ +export const zori: ZoriCoreAPI = { + track, + identify, + getVisitorId, + getSessionId, + setConsent, + optOut, + hasConsent, +}; + +export default zori; diff --git a/astro/package.json b/astro/package.json new file mode 100644 index 0000000..2f0040a --- /dev/null +++ b/astro/package.json @@ -0,0 +1,49 @@ +{ + "name": "@zorihq/astro", + "version": "1.0.0", + "description": "ZoriHQ Analytics for Astro", + "main": "index.ts", + "types": "index.ts", + "exports": { + ".": { + "import": "./index.ts", + "require": "./index.ts", + "types": "./index.ts" + }, + "./ZoriScript.astro": "./ZoriScript.astro" + }, + "files": [ + "index.ts", + "ZoriScript.astro", + "README.md", + "package.json" + ], + "scripts": { + "build": "tsc", + "prepublishOnly": "echo 'Publishing source files'", + "test": "echo 'Tests not yet configured'" + }, + "keywords": [ + "analytics", + "astro", + "tracking", + "zorihq" + ], + "author": "ZoriHQ", + "license": "MIT", + "peerDependencies": { + "astro": ">=3.0.0" + }, + "dependencies": { + "@zorihq/types": "^1.0.0" + }, + "devDependencies": { + "astro": "^4.0.0", + "typescript": "^5.0.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/zorihq/script.git", + "directory": "astro" + } +} diff --git a/astro/tsconfig.json b/astro/tsconfig.json new file mode 100644 index 0000000..3517570 --- /dev/null +++ b/astro/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"] +}