tessl install tessl/npm-posthog-js@1.335.0PostHog Browser JS Library is a comprehensive browser analytics and feature management SDK that enables developers to capture user events, track product analytics, manage feature flags, record session replays, and implement feedback mechanisms like surveys and conversations in web applications.
Handling edge cases, race conditions, and advanced scenarios.
// ❌ Bad: Check flag before it's loaded
function Component() {
const enabled = posthog.isFeatureEnabled('new-ui'); // May be undefined
return enabled ? <NewUI /> : <OldUI />;
}
// ✅ Good: Wait for flags to load
function Component() {
const [flagsLoaded, setFlagsLoaded] = useState(false);
const [enabled, setEnabled] = useState(false);
useEffect(() => {
const unsubscribe = posthog.onFeatureFlags((flags) => {
setEnabled(flags['new-ui'] || false);
setFlagsLoaded(true);
});
return unsubscribe;
}, []);
if (!flagsLoaded) return <Loading />;
return enabled ? <NewUI /> : <OldUI />;
}// Ensure PostHog is ready before use
async function initializeApp() {
return new Promise<void>((resolve) => {
posthog.init('token', {
loaded: () => {
console.log('PostHog ready');
resolve();
}
});
});
}
// Wait for initialization
await initializeApp();
posthog.capture('app_initialized');// Handle offline scenarios gracefully
window.addEventListener('online', () => {
console.log('Back online - PostHog will resume');
posthog.capture('connection_restored');
});
window.addEventListener('offline', () => {
console.log('Offline - PostHog will queue events');
// Events are automatically queued and sent when connection returns
});
// Check connectivity before critical operations
if (navigator.onLine) {
posthog.reloadFeatureFlags();
}// Fallback when API is unreachable
posthog.init('token', {
api_host: 'https://us.i.posthog.com',
on_request_error: (response) => {
console.error('PostHog request failed:', response.statusCode);
// Log to alternative service
logToBackupService({
error: 'PostHog unreachable',
status: response.statusCode
});
}
});// Handle blocked storage
try {
posthog.init('token', {
persistence: 'localStorage+cookie'
});
} catch (error) {
console.warn('Storage blocked, using memory persistence');
// Fallback to memory-only
posthog.init('token', {
persistence: 'memory'
});
}// Handle third-party cookie restrictions
posthog.init('token', {
persistence: 'localStorage', // Avoid cookies
cross_subdomain_cookie: false,
secure_cookie: true
});// Singleton pattern for PostHog
let posthogInitialized = false;
export function initPostHog() {
if (posthogInitialized) {
console.warn('PostHog already initialized');
return;
}
posthog.init('token', {
loaded: () => {
posthogInitialized = true;
}
});
}
// Safe to call multiple times
initPostHog();
initPostHog(); // Will warn and skip// Track session transitions
let currentSessionId = posthog.get_session_id();
posthog.onSessionId((newSessionId, windowId) => {
console.log('Session changed:', {
old: currentSessionId,
new: newSessionId
});
// Track transition
posthog.capture('session_transitioned', {
previous_session: currentSessionId,
new_session: newSessionId,
window_id: windowId
});
currentSessionId = newSessionId;
});// Properly link anonymous and identified users
async function handleSignup(userData: SignupData) {
const anonymousId = posthog.get_distinct_id();
// Create user account
const user = await createAccount(userData);
// Link anonymous activity to new user
posthog.alias(user.id);
// Identify with properties
posthog.identify(user.id, {
email: user.email,
name: user.name
}, {
signup_date: new Date().toISOString(),
initial_referrer: document.referrer
});
// Track the conversion
posthog.capture('user_signed_up', {
previous_id: anonymousId,
new_id: user.id
});
}// Handle same user across devices
function identifyAcrossDevices(userId: string, deviceId: string) {
posthog.identify(userId, {
email: user.email
});
// Track device
posthog.capture('device_identified', {
device_id: deviceId,
device_type: getDeviceType(),
first_seen: isNewDevice(deviceId)
});
// Set device as super property
posthog.register({
device_id: deviceId
});
}// ❌ Bad: Conflicting configuration
posthog.init('token', {
disable_persistence: true,
persistence: 'localStorage' // Ignored!
});
// ✅ Good: Consistent configuration
posthog.init('token', {
disable_persistence: false,
persistence: 'localStorage'
});
// ✅ Good: Validate configuration
function validateAndInit(config: Partial<PostHogConfig>) {
if (config.disable_persistence && config.persistence !== 'memory') {
console.warn('Conflict: disable_persistence overrides persistence setting');
config.persistence = 'memory';
}
posthog.init('token', config);
}// Prevent event flooding
class EventThrottler {
private lastCapture = new Map<string, number>();
private throttleMs = 1000;
capture(event: string, properties: Properties = {}) {
const now = Date.now();
const last = this.lastCapture.get(event);
if (last && (now - last) < this.throttleMs) {
console.log('Throttling event:', event);
return;
}
this.lastCapture.set(event, now);
posthog.capture(event, properties);
}
}
const throttler = new EventThrottler();
// Use for high-frequency events
window.addEventListener('scroll', () => {
throttler.capture('scrolled', {
position: window.scrollY
});
});// Manage memory in long-running sessions
class SessionManager {
private sessionStartTime = Date.now();
private readonly MAX_SESSION_TIME = 4 * 60 * 60 * 1000; // 4 hours
initialize() {
setInterval(() => {
this.checkSessionAge();
}, 60000); // Check every minute
}
checkSessionAge() {
const age = Date.now() - this.sessionStartTime;
if (age > this.MAX_SESSION_TIME) {
console.log('Session too old, resetting');
// Capture session end
posthog.capture('long_session_reset', {
session_duration_ms: age
});
// Reset PostHog state
posthog.reset();
// Restart
this.sessionStartTime = Date.now();
}
}
}// Check for required features
function checkBrowserSupport(): boolean {
const required = {
localStorage: typeof window.localStorage !== 'undefined',
fetch: typeof window.fetch !== 'undefined',
Promise: typeof Promise !== 'undefined',
JSON: typeof JSON !== 'undefined'
};
const unsupported = Object.entries(required)
.filter(([key, supported]) => !supported)
.map(([key]) => key);
if (unsupported.length > 0) {
console.error('Unsupported features:', unsupported);
return false;
}
return true;
}
// Initialize only if supported
if (checkBrowserSupport()) {
posthog.init('token');
} else {
console.warn('Browser not supported, PostHog disabled');
}// Adapt to device capabilities
function getOptimalConfig(): Partial<PostHogConfig> {
const isLowEnd = (
navigator.deviceMemory && navigator.deviceMemory < 4 ||
navigator.hardwareConcurrency && navigator.hardwareConcurrency < 4
);
if (isLowEnd) {
return {
session_recording: {
enabled: false // Disable expensive features
},
capture_heatmaps: false,
autocapture: {
capture_clicks: true,
rageclicks: false,
dead_clicks: false
}
};
}
return {
session_recording: {
enabled: true,
recordCanvas: true
},
capture_heatmaps: true
};
}
posthog.init('token', getOptimalConfig());// Complete PostHog mock
const createPostHogMock = () => ({
init: jest.fn(),
capture: jest.fn(),
identify: jest.fn(),
alias: jest.fn(),
reset: jest.fn(),
isFeatureEnabled: jest.fn(() => false),
getFeatureFlag: jest.fn(() => undefined),
onFeatureFlags: jest.fn((cb) => {
cb({}, {});
return () => {};
}),
startSessionRecording: jest.fn(),
stopSessionRecording: jest.fn(),
get_distinct_id: jest.fn(() => 'test-user'),
get_session_id: jest.fn(() => 'test-session'),
featureFlags: {
hasLoadedFlags: true,
getFlags: jest.fn(() => [])
}
});
// Use in tests
beforeEach(() => {
(window as any).posthog = createPostHogMock();
});// Sanitize user input before tracking
function sanitizeProperties(properties: Properties): Properties {
const sanitized: Properties = {};
for (const [key, value] of Object.entries(properties)) {
if (typeof value === 'string') {
// Remove potential XSS
sanitized[key] = value
.replace(/<script[^>]*>.*?<\/script>/gi, '')
.replace(/<[^>]+>/g, '')
.substring(0, 1000); // Limit length
} else {
sanitized[key] = value;
}
}
return sanitized;
}
// Use when tracking user-generated content
posthog.capture('comment_posted', sanitizeProperties({
comment_text: userInput,
user_id: userId
}));// Run PostHog alongside existing analytics
function dualTrack(event: string, properties: Properties) {
// Track to both systems during migration
posthog.capture(event, properties);
legacyAnalytics.track(event, properties);
}
// Gradually phase out old system
const migrationPercent = 50; // 50% on PostHog
function smartTrack(event: string, properties: Properties) {
if (Math.random() * 100 < migrationPercent) {
posthog.capture(event, properties);
} else {
legacyAnalytics.track(event, properties);
}
}