Shared tracking utilities for AACI Group projects with React Context support
npm install @aacigroup/aaci_sharedReact Context-based tracking and magic-link library for frontend and backend projects.

| Client: Frontend Side – React | Client: Backend Side | API Server: Backend Side |
| --- | --- | --- |
---
- Client: Frontend (React) - React Context, hooks, and components
- Client: Backend (Supabase Edge Functions) - Magic Links, 2FA, and notifications in edge functions
- API Server (Backend Services) - Type-safe API handlers with validation
---
---
``bash`
npm install @aacigroup/aaci_shared react
bash
npm install @aacigroup/aaci_shared react
`$3
`env
.env file
VITE_LEAD_CAPTURE_API_URL=https://your-api.com/leads
VITE_LEAD_CAPTURE_API_KEY=your-lead-api-key
VITE_PROJECT_NAME=MyProject
VITE_POSTHOG_KEY=phc_your_production_key
VITE_POSTHOG_DEV_KEY=phc_your_development_key
VITE_PRODUCTION_DOMAINS=myproject.com,www.myproject.com
`$3
`javascript
// App.tsx
import { TrackingProvider } from '@aacigroup/aaci_shared/react';function App() {
const trackingConfig = {
apiUrl: import.meta.env.VITE_LEAD_CAPTURE_API_URL,
apiKey: import.meta.env.VITE_LEAD_CAPTURE_API_KEY,
projectName: import.meta.env.VITE_PROJECT_NAME,
posthogKey: import.meta.env.VITE_POSTHOG_KEY,
posthogDevKey: import.meta.env.VITE_POSTHOG_DEV_KEY,
productionDomains: import.meta.env.VITE_PRODUCTION_DOMAINS.split(',').map(d => d.trim())
};
return (
);
}
`$3
`javascript
// ContactForm.tsx
import { useLeadTracker, usePostHog } from '@aacigroup/aaci_shared/react';function ContactForm() {
const tracker = useLeadTracker();
const analytics = usePostHog();
const handleSubmit = async (formData) => {
// Track lead
await tracker.trackLeadAndAddress({
lead_type: 'contact',
email: formData.email,
first_name: formData.firstName
});
// Track analytics
analytics.trackEvent('contact_form_submitted');
};
return
;
}
`React Context Setup
$3
`javascript
// App.tsx
import { TrackingProvider } from '@aacigroup/aaci_shared/react';function App() {
// Create config from environment variables
const trackingConfig = {
// Lead Capture API settings
apiUrl: import.meta.env.VITE_LEAD_CAPTURE_API_URL,
apiKey: import.meta.env.VITE_LEAD_CAPTURE_API_KEY,
projectName: import.meta.env.VITE_PROJECT_NAME,
// PostHog Analytics settings
posthogKey: import.meta.env.VITE_POSTHOG_KEY,
posthogDevKey: import.meta.env.VITE_POSTHOG_DEV_KEY, // Optional: for development
posthogApiHost: import.meta.env.VITE_POSTHOG_API_HOST, // Optional: for self-hosted PostHog
// Environment detection
productionDomains: import.meta.env.VITE_PRODUCTION_DOMAINS.split(',').map(d => d.trim())
};
return (
);
}
`$3
The library automatically uses different PostHog instances based on your environment:
- Production Environment: Uses
VITE_POSTHOG_KEY (only on domains listed in VITE_PRODUCTION_DOMAINS)
- Development/Staging: Uses VITE_POSTHOG_DEV_KEY (on all other domains/localhost)This allows you to:
- Keep production analytics clean and separate from development data
- Track development/staging events in a separate PostHog project
- Debug analytics issues without affecting production data
Configuration Options:
`env
Required: Production PostHog key
VITE_POSTHOG_KEY=phc_your_production_keyOptional: Development PostHog key (if not provided, PostHog won't initialize in dev)
VITE_POSTHOG_DEV_KEY=phc_your_development_key
`$3
Add
?internal=1 to any URL to mark traffic as internal and exclude it from analytics:`
https://yoursite.com?internal=1 // Marks as internal
https://yoursite.com?internal=true // Marks as internal
https://yoursite.com?internal=0 // Marks as external
https://yoursite.com?internal=false // Marks as external
`
Setting persists across page visits until browser storage is cleared.$3
`javascript
// ContactForm.tsx
import {
useLeadTracker,
usePostHog,
useStoredLead,
useStoredAddress
} from '@aacigroup/aaci_shared/react';function ContactForm() {
const tracker = useLeadTracker();
const analytics = usePostHog();
const storedLead = useStoredLead(); // TrackLeadParams | null
const storedAddress = useStoredAddress(); // AddressInput | null
const handleSubmit = async (formData) => {
// Track lead submission
await tracker.trackLeadAndAddress({
lead_type: 'contact',
email: formData.email,
first_name: formData.firstName
});
// Track analytics event
analytics.trackEvent('contact', { has_email: !!email });
};
// Check if user has previously stored data
const hasStoredData = storedLead || storedAddress;
return (
);
}
`Feature Flags (Optional)
The library includes optional PostHog-powered feature flags that integrate seamlessly with the tracking system.
$3
Feature flags are optional and must be explicitly added by wrapping your app with
FeatureFlagsProvider inside the TrackingProvider:`javascript
// App.tsx
import {
TrackingProvider,
FeatureFlagsProvider
} from '@aacigroup/aaci_shared/react';function App() {
const trackingConfig = {
// ... your tracking config
};
return (
);
}
`Important:
FeatureFlagsProvider must be inside TrackingProvider - it uses the same PostHog instance for consistency.$3
`javascript
import { useFeatureFlags } from '@aacigroup/aaci_shared/react';function MyComponent() {
const { isFeatureFlagEnabled, getFeatureFlag } = useFeatureFlags();
// Simple boolean flag
const showNewDashboard = isFeatureFlagEnabled('new-dashboard');
// Multi-variant flag (string/boolean values)
const buttonColor = getFeatureFlag('button-color'); // 'red', 'blue', true, false, etc.
return (
{showNewDashboard && }
);
}
`$3
To create and manage feature flags in PostHog:
1. Access PostHog Dashboard
- Go to PostHog
- Navigate to the project matching your API key
2. Create Feature Flag
- Go to "Feature Flags" in the left sidebar
- Click "New Feature Flag"
- Enter a flag key (e.g.,
new-dashboard, button-color)3. Configure Release
- For simple on/off flags: Set release condition to "100% of users"
- For A/B testing: Configure percentage splits
- For gradual rollouts: Start with lower percentages and increase over time
4. Save and Deploy
- Click "Save" - flags are immediately available
- No code deployment needed for flag value changes
$3
`javascript
// Boolean flags (most common)
const showNewFeature = isFeatureFlagEnabled('new-feature');
const enableBetaMode = isFeatureFlagEnabled('beta-mode');// Multi-variant flags
const theme = getFeatureFlag('ui-theme'); // 'dark', 'light', 'auto'
const buttonStyle = getFeatureFlag('btn-style'); // 'rounded', 'square', 'pill'
const pricing = getFeatureFlag('pricing-tier'); // 'basic', 'premium', 'enterprise'
// Use in conditional rendering
return (
{showNewFeature && }
);
`$3
If
FeatureFlagsProvider is used outside TrackingProvider, you'll see helpful error messages:`javascript
// ❌ Wrong - will show console.error
// Console error: "FeatureFlagsProvider must be used within TrackingProvider..."
`$3
Access the PostHog instance directly if needed:
`javascript
import { usePostHogFeatureFlags, usePostHog } from '@aacigroup/aaci_shared/react';function AdvancedComponent() {
const posthog = usePostHogFeatureFlags();
const analytics = usePostHog();
// Access PostHog methods directly
const flagValue = posthog.getInstance().getFeatureFlag('complex-flag');
// Check which PostHog environment is being used
const envInfo = analytics.getEnvironmentInfo();
console.log(
Using ${envInfo.environment} PostHog instance, envInfo);
return Advanced usage;
}
`Data Types
$3
The library includes a portable question types system for managing dynamic forms and questionnaires.
`typescript
// Import all question types
import * as QuestionTypes from '@aacigroup/aaci_shared/questionTypes';// Import specific types
import {
TemplateSource,
QuestionTemplateData,
EntityQuestionsMap,
TemplateQuestion
} from '@aacigroup/aaci_shared/questionTypes';
`Available exports:
- All type definitions from
types.ts
- Helper functions from helpers.ts
- Validators from validators.ts
- Defaults from defaults.ts
- Writers from writers.ts
- Session types from session.ts
- Template types from template.ts (QuestionTemplate, EntityQuestionsMap, TemplateQuestion, etc.)
- Answer validators from answerValidators/
- Conditions from conditions.ts
- Condition helpers from conditionHelpers.ts$3
`typescript
interface TrackLeadParams {
lead_type: string; // Required: Type of lead
first_name?: string; // Optional: First name
last_name?: string; // Optional: Last name
email?: string; // Optional: Email address
phone?: string; // Optional: Phone number
extra_data?: Record; // Optional: Additional custom data
}
`Common
lead_type examples (free string):
- 'policy_review' - Insurance policy review requests
- 'address_check' - Property address verification
- 'contact' - General contact form submissions
- 'signup' - User registration/signup
- 'consultation' - Service consultation requests
- 'quote' - Price quote requestsNote:
lead_type is a free string - you can use any value that makes sense for your application.Validation Rules:
-
lead_type is always required
- Either email OR phone is required when tracking a lead$3
`typescript
// Base address interface - only address-related fields
interface Address {
full_address: string; // Required: Complete address string
place_id: string; // Required: Google Places API place ID
street_address: string; // Required: Street address component
street_address_2?: string; // Optional: Street address line 2 (apt, suite, etc.)
city: string; // Required: City
state: string; // Required: State/Province
zip_code: string; // Required: ZIP/Postal code
county?: string; // Optional: County
country?: string; // Optional: Country
}// Address input for tracking - includes source and extra_data
interface AddressInput extends Address {
source: string; // Required: Lead type that generated this address
extra_data?: Record; // Optional: Additional address data
}
`Usage:
- Use
Address for pure address data (display, validation)
- Use AddressInput for tracking operations that require source field$3
`typescript
interface SessionData {
ip?: string; // Optional: User's IP address
user_agent?: string; // Optional: Browser user agent
browser?: string; // Optional: Browser name
browser_version?: string; // Optional: Browser version
os?: string; // Optional: Operating system
device?: string; // Optional: Device type
referrer?: string; // Optional: Referring page URL
utm_source?: string; // Optional: UTM source parameter
utm_medium?: string; // Optional: UTM medium parameter
utm_campaign?: string; // Optional: UTM campaign parameter
landing_page?: string; // Optional: First page visited
current_page?: string; // Optional: Current page URL
session_id?: string; // Optional: Session identifier
timestamp?: string; // Optional: Timestamp of action
distinct_id?: string; // Optional: User identifier
gclid?: string; // Optional: Google Ads click ID
fbclid?: string; // Optional: Facebook click ID
fbc?: string; // Optional: Facebook browser cookie
fbp?: string; // Optional: Facebook pixel cookie
is_internal?: boolean; // Optional: Internal traffic flag (auto-detected from URL)
}
`Common
source examples (free string):
- 'policy_review' - When tracking address from policy review forms
- 'address_check' - For standalone address verification
- 'contact' - From general contact forms
- 'signup' - During user registration
- 'consultation' - From consultation request forms
- 'quote' - From quote request formsNote:
source is a free string - you can use any value to identify where the address came from. It may match lead_type by design but doesn't have to.Validation Rules:
-
full_address is always required
- place_id is always required
- street_address is always required
- city is always required
- state is always required
- zip_code is always required
- source is always requiredUsage Examples
`javascript
import { useLeadTracker, usePostHog } from '@aacigroup/aaci_shared/react';function MyComponent() {
const tracker = useLeadTracker();
const analytics = usePostHog();
// 1. Track lead only - Basic contact form
const handleContactForm = async (formData) => {
await tracker.trackLeadAndAddress({
lead_type: 'contact',
email: formData.email,
first_name: formData.firstName
});
analytics.trackEvent('contact_form_submitted');
};
// 2. Track address only - Address verification
const handleAddressCheck = async (address) => {
await tracker.trackLeadAndAddress(undefined, {
full_address: address,
place_id: 'ChIJd8BlQ2BZwokRAFUEcm_qrcA', // Google Places API ID
street_address: '123 Main St',
city: 'New York',
state: 'NY',
zip_code: '10001',
source: 'address_check'
});
analytics.trackEvent('address_checked');
};
// 3. Track both together - Policy review with address
const handlePolicyReview = async (leadData, addressData) => {
await tracker.trackLeadAndAddress({
lead_type: 'policy_review',
email: leadData.email,
phone: leadData.phone,
extra_data: { policy_number: leadData.policyNumber }
}, {
full_address: addressData.fullAddress,
place_id: addressData.placeId,
street_address: addressData.streetAddress,
city: addressData.city,
zip_code: addressData.zipCode,
source: 'policy_review'
});
analytics.trackEvent('policy_review_started', {
has_phone: !!leadData.phone
});
};
// 4. Multi-step forms
const handleStepOne = async (leadData) => {
// Step 1: Collect lead info
await tracker.trackLeadAndAddress({
lead_type: 'quote',
email: leadData.email
});
};
const handleStepTwo = async (addressData) => {
// Step 2: Add address (automatically merges with saved lead)
await tracker.trackLeadAndAddress(undefined, {
full_address: addressData.address,
place_id: addressData.placeId,
street_address: addressData.street,
city: addressData.city,
zip_code: addressData.zipCode,
source: 'quote'
});
analytics.trackEvent('quote_completed');
};
return
...;
}
`Data Persistence
Lead and address data are automatically saved to browser localStorage when successfully tracked:
- Lead data - Saved to
aaci_saved_lead after successful API response
- Address data - Saved to aaci_saved_address after successful API response
- Smart merging - If you track only address later, it automatically attaches to saved lead
- Session persistence - Data persists across page reloads within the same browser$3
`javascript
import { useStoredLead, useStoredAddress } from '@aacigroup/aaci_shared/react';function MyComponent() {
const storedLead = useStoredLead(); // Returns TrackLeadParams | null
const storedAddress = useStoredAddress(); // Returns Address | null
// Use stored data to pre-fill forms or show personalized content
if (storedLead) {
console.log('Previous lead:', storedLead.email);
console.log('Lead type:', storedLead.lead_type);
}
if (storedAddress) {
console.log('Previous address:', storedAddress.full_address);
console.log('Address source:', storedAddress.source);
}
}
`$3
-
aaci_saved_lead - Contains the last successfully tracked lead data
- aaci_saved_address - Contains the last successfully tracked address data`javascript
// Example: Multi-step form automatically merges data
// Step 1: Track lead (saves to localStorage)
await tracker.trackLeadAndAddress({ lead_type: 'quote', email: 'user@example.com' });// Step 2: Track address (merges with saved lead automatically)
await tracker.trackLeadAndAddress(undefined, {
full_address: '123 Main St, New York, NY 10001',
place_id: 'ChIJd8BlQ2BZwokRAFUEcm_qrcA',
street_address: '123 Main St',
city: 'New York',
zip_code: '10001',
source: 'quote'
});
`Magic Links
The library includes Magic Link functionality for secure, token-based, login-free access to personalized pages. Magic links are created and validated centrally in your backend and tied to a user profile.
> Note: Magic Link is a separate hook from TrackingContext. It requires its own configuration.
$3
Magic links provide:
- Customer access tokens - For public URLs to personalized content
- Admin tokens (optional) - For elevated access/override links
- URL pattern binding - Tokens are only valid within their expected URL structure
- Automatic user resolution - Links to user profile by email/phone
$3
`javascript
import { useMagicLink, MagicLinkConfig } from '@aacigroup/aaci_shared/react';function MagicLinkExample() {
// Magic Link requires its own config (separate from TrackingConfig)
const magicLinkConfig: MagicLinkConfig = {
apiUrl: import.meta.env.VITE_LEAD_CAPTURE_API_URL,
apiKey: import.meta.env.VITE_LEAD_CAPTURE_API_KEY,
projectName: import.meta.env.VITE_PROJECT_NAME
};
const magicLink = useMagicLink(magicLinkConfig);
// Create a magic link for a customer
const handleCreateLink = async () => {
const result = await magicLink.createMagicLink({
email: 'customer@example.com',
url_pattern: 'https://myapp.com/session/{token}',
admin_url_pattern: 'https://myapp.com/session/{token}/admin/{admin_token}', // optional
expires_at: '2025-12-31T23:59:59.000Z' // optional
});
if (result.status === 'ok') {
console.log('Customer URL:', result.url);
console.log('Admin URL:', result.admin_url); // if admin_url_pattern was provided
console.log('Token ID:', result.magic_link_token_id);
}
};
// Validate a magic link token from URL (extract token from router params)
// Example: if your route is /session/:sessionId/:token
import { useParams } from 'react-router-dom';
function SessionPage() {
const { token } = useParams(); // token from route: /session/:sessionId/:token
const handleValidateLink = async () => {
const currentUrl = window.location.href;
const result = await magicLink.validateMagicLink({
token,
current_url: currentUrl,
mode: 'customer' // or 'admin' for admin access
// pin: '1234' // Include PIN if security_mode requires it
});
if (result.valid) {
console.log('Valid! User ID:', result.person_profile_id);
console.log('Link data:', result.data); // Data stored in magic link data
console.log('Security mode:', result.security_mode); // 'basic', 'pin_light', etc.
// Render personalized content
} else {
console.log('Invalid:', result.reason); // 'expired', 'revoked', 'url_mismatch', 'not_found', 'pin_required', 'pin_invalid'
}
};
// Call handleValidateLink on mount or button click
// useEffect(() => { handleValidateLink(); }, []);
// or
return null;
}
return
...;
}
`$3
`javascript
interface CreateMagicLinkParams {
project_name?: string; // Optional – auto-populated from config
email?: string; // Optional – used to resolve user
phone?: string; // Optional – alternative user lookup url_pattern: string; // Required – must contain "{token}"
admin_url_pattern?: string; // Optional – must contain "{admin_token}"
expires_at?: string; // Optional ISO timestamp
extra_data?: Record; // Optional – stored in magic link data
session_data?: SessionData; // Optional – auto-populated if not provided
temp_security_mode?: SecurityMode; // Optional – temporary override for security_mode
}
`Rules:
- At least one of:
email or phone is required
- url_pattern is required and must contain {token}
- If admin_url_pattern is provided, it must contain {admin_token} (and may also include {token})
- session_data is automatically collected from the browser if not explicitly providedResponse:
`javascript
interface CreateMagicLinkResponse {
status: 'ok' | 'error';
project_name?: string; // Project identifier
magic_link_token_id?: string; // ID of the created magic link
url?: string; // Resolved customer URL
admin_url?: string; // Resolved admin URL (if admin_url_pattern provided)
expires_at?: string; // Expiration timestamp (if set)
security_mode?: SecurityMode; // Security mode applied to this magic link
pin_code?: string; // PIN code (only returned if security_mode is pin_light or pin_strict)
message?: string;
errors?: Array<{ field: string; message: string }>;
}
`$3
`javascript
interface ValidateMagicLinkParams {
project_name?: string; // Optional – auto-populated from config
token: string; // The token from URL
current_url: string; // Full URL being accessed
mode?: 'customer' | 'admin'; // Default: 'customer'
pin?: string; // PIN code (required if security_mode is pin_light or pin_strict)
session_data?: SessionData; // Optional – auto-populated if not provided
}
`Rules:
-
mode = 'customer' → validates against token + url_pattern
- mode = 'admin' → validates against admin_token + admin_url_pattern
- Link must: exist, not be expired, not be revoked, match the expected URL pattern
- session_data is automatically collected from the browser if not explicitly providedResponse:
`javascript
interface ValidateMagicLinkResponse {
status: 'ok' | 'error';
valid: boolean;
project_name?: string; // Project identifier
magic_link_token_id?: string;
person_profile_id?: string;
data?: Record; // Data stored in magic link data
security_mode?: SecurityMode; // Security mode of the magic link
message?: string;
reason?: 'expired' | 'revoked' | 'url_mismatch' | 'not_found' | 'pin_required' | 'pin_invalid'; // if invalid
}
`SecurityMode Type:
`javascript
type SecurityMode = 'basic' | 'pin_light' | 'pin_strict' | '2fa_light' | '2fa_strict';
`$3
`javascript
import { useMagicLink } from '@aacigroup/aaci_shared/react';
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom'; // or your router of choicefunction ProtectedSessionPage() {
const magicLink = useMagicLink();
const { token, adminToken } = useParams(); // Get tokens from React Router
const [accessState, setAccessState] = useState<'loading' | 'valid' | 'invalid'>('loading');
const [personId, setPersonId] = useState(null);
const [linkData, setLinkData] = useState | null>(null);
const [isAdmin, setIsAdmin] = useState(false);
useEffect(() => {
const validateAccess = async () => {
const currentUrl = window.location.href;
// Try admin token first if available
if (adminToken) {
const result = await magicLink.validateMagicLink({
token: adminToken,
current_url: currentUrl,
mode: 'admin'
});
if (result.valid) {
setAccessState('valid');
setPersonId(result.person_profile_id || null);
setLinkData(result.data || null);
setIsAdmin(true);
return;
}
}
// Fall back to customer token
if (token) {
const result = await magicLink.validateMagicLink({
token,
current_url: currentUrl,
mode: 'customer'
});
if (result.valid) {
setAccessState('valid');
setPersonId(result.person_profile_id || null);
setLinkData(result.data || null);
return;
}
}
setAccessState('invalid');
};
validateAccess();
}, [token, adminToken]);
if (accessState === 'loading') return
Validating access...;
if (accessState === 'invalid') return Access denied. Invalid or expired link.; return (
{isAdmin && Admin Mode}
Welcome to your personalized session
{/ Render personalized content using personId and linkData /}
);
}
`$3
- Projects must NOT store tokens - Only store
magic_link_token_id in your database entities
- Tokens are URL-bound - A token is only valid when accessed via its designated URL pattern
- Tokens are strong - 32-64 character alphanumeric strings
- PIN Security Modes - When security_mode is pin_light or pin_strict, include the pin parameter in validation requests
- 2FA Security Modes - When security_mode is 2fa_light or 2fa_strict, 2FA verification is required before access
- Your backend should be the single source of truth for token storage and validationNon-React and Backend Usage
For Client: Backend Side and API Server: Backend Side usage patterns (including MagicLink usage in edge/API functions and server-only validation), see:
-
CLIENT_BACKEND.md
- API_SERVER.mdTroubleshooting
$3
If you see console errors like:
`
GET https://your-api.com/functions/v1/capture-lead-with-address/array/... 401 (Unauthorized)
POST https://your-api.com/functions/v1/capture-lead-with-address/flags/... 401 (Unauthorized)
[PostHog.js] Bad HTTP status: 401 {"status":"error","message":"Unauthorized"}
`Cause: PostHog is trying to make requests to your lead capture API instead of PostHog's servers.
Solutions:
1. Don't set
posthogApiHost unless you have a self-hosted PostHog instance:
`javascript
// ❌ Wrong - this will cause 401 errors
const trackingConfig = {
apiUrl: 'https://your-api.com/leads',
posthogApiHost: 'https://your-api.com/leads', // Don't do this!
posthogKey: 'phc_...'
}; // ✅ Correct - let PostHog use its default servers
const trackingConfig = {
apiUrl: 'https://your-api.com/leads',
// posthogApiHost: not needed for PostHog Cloud
posthogKey: 'phc_...'
};
`2. For PostHog Cloud users (most common):
- Remove or don't set
posthogApiHost in your config
- PostHog will automatically use https://us.i.posthog.com3. For self-hosted PostHog users:
`javascript
const trackingConfig = {
apiUrl: 'https://your-api.com/leads', // Your API
posthogApiHost: 'https://your-posthog.com', // Your PostHog instance
posthogKey: 'phc_...'
};
`$3
Environment Variables Not Loading:
- Make sure your
.env file is in the project root
- Restart your development server after adding new environment variables
- Verify variable names start with VITE_ for Vite projectsPostHog Not Initializing:
- Check browser console for initialization messages
- Verify your PostHog API key format:
phc_...
- Make sure productionDomains matches your actual domainLead Tracking API Errors:
- Verify
VITE_LEAD_CAPTURE_API_URL points to your actual API
- Check that VITE_LEAD_CAPTURE_API_KEY` is correctly setISC