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.
Comprehensive best practices for using PostHog.js in production applications, covering initialization, event tracking, feature flags, session recording, privacy, and performance optimization.
// Create environment-aware configuration
const getConfig = (): Partial<PostHogConfig> => {
const baseConfig: Partial<PostHogConfig> = {
api_host: process.env.POSTHOG_API_HOST || 'https://us.i.posthog.com',
autocapture: true,
capture_pageview: 'history_change',
person_profiles: 'identified_only'
};
if (process.env.NODE_ENV === 'production') {
return {
...baseConfig,
debug: false,
session_recording: {
enabled: true,
maskAllInputs: true,
consoleLogRecordingEnabled: false
},
advanced_disable_toolbar_metrics: true
};
}
return {
...baseConfig,
debug: true,
session_recording: {
enabled: true,
consoleLogRecordingEnabled: true
}
};
};
posthog.init(process.env.POSTHOG_API_KEY!, getConfig());// Handle initialization failures gracefully
function initPostHogSafely() {
try {
posthog.init(process.env.POSTHOG_API_KEY!, {
loaded: (posthog) => {
console.log('PostHog initialized successfully');
},
on_request_error: (response) => {
// Log but don't crash
console.error('PostHog error:', response.statusCode);
}
});
} catch (error) {
console.error('PostHog initialization failed:', error);
// Application continues without analytics
}
}// Use a consistent naming convention
const EventNames = {
// User actions (verb_noun)
BUTTON_CLICKED: 'button_clicked',
FORM_SUBMITTED: 'form_submitted',
SEARCH_PERFORMED: 'search_performed',
// Page views
PAGE_VIEWED: 'page_viewed',
// Business events
PURCHASE_COMPLETED: 'purchase_completed',
SUBSCRIPTION_STARTED: 'subscription_started'
} as const;
// Type-safe event tracking
posthog.capture(EventNames.BUTTON_CLICKED, {
button_id: 'signup',
button_location: 'header'
});// Include comprehensive context
function trackUserAction(action: string, context: Record<string, any> = {}) {
posthog.capture(action, {
...context,
// Automatic context
page_path: window.location.pathname,
page_url: window.location.href,
referrer: document.referrer,
viewport_height: window.innerHeight,
viewport_width: window.innerWidth,
timestamp: new Date().toISOString()
});
}// ❌ Bad: Track every mouse move
document.addEventListener('mousemove', (e) => {
posthog.capture('mouse_moved', { x: e.clientX, y: e.clientY });
});
// ✅ Good: Track meaningful interactions
document.querySelectorAll('.cta-button').forEach(button => {
button.addEventListener('click', () => {
posthog.capture('cta_clicked', {
button_text: button.textContent,
button_location: button.dataset.location
});
});
});// Always provide defaults
function getFeature(flagKey: string, defaultValue: boolean = false): boolean {
const value = posthog.isFeatureEnabled(flagKey);
return value !== undefined ? value : defaultValue;
}
// Wait for flags before critical decisions
async function initializeCriticalFeature() {
// Wait for flags to load
if (!posthog.featureFlags.hasLoadedFlags) {
await new Promise(resolve => {
posthog.onFeatureFlags(() => resolve(undefined))();
});
}
if (getFeature('critical-feature', false)) {
await loadCriticalFeature();
}
}// Use flags for dynamic configuration
interface AppConfig {
maxRetries: number;
timeout: number;
endpoint: string;
}
function getAppConfig(): AppConfig {
const flagPayload = posthog.getFeatureFlagPayload('app-config') as Partial<AppConfig>;
const defaults: AppConfig = {
maxRetries: 3,
timeout: 5000,
endpoint: '/api/v1'
};
return {
...defaults,
...flagPayload
};
}// Track experiment exposure
function trackExperimentExposure(experimentKey: string) {
const variant = posthog.getFeatureFlag(experimentKey);
if (variant) {
posthog.capture('experiment_exposure', {
experiment_key: experimentKey,
variant: variant,
session_id: posthog.get_session_id(),
exposure_time: Date.now()
});
}
}// Configure with privacy in mind
posthog.init('token', {
session_recording: {
enabled: true,
// Mask sensitive inputs
maskAllInputs: true,
// Block sensitive sections
blockSelector: '.sensitive-data, .payment-info, .ssn',
// Mask specific text
maskTextSelector: '.email, .phone, .address',
// Control network capture
networkPayloadCapture: {
recordHeaders: false,
recordBody: ['application/json'] // Only JSON
}
}
});// Only record important user flows
const recordedPaths = ['/checkout', '/onboarding', '/dashboard'];
router.on('route', (route) => {
const shouldRecord = recordedPaths.some(path =>
route.path.startsWith(path)
);
if (shouldRecord && !posthog.sessionRecordingStarted()) {
posthog.startSessionRecording();
} else if (!shouldRecord && posthog.sessionRecordingStarted()) {
posthog.stopSessionRecording();
}
});// Always link session replays to errors
window.addEventListener('error', (event) => {
posthog.captureException(event.error, {
session_replay_url: posthog.get_session_replay_url({
withTimestamp: true,
timestampLookBack: 30
}),
session_id: posthog.get_session_id(),
page_url: window.location.href,
user_agent: navigator.userAgent
});
});// Identify progressively as you learn more
class UserIdentification {
identifyAnonymous() {
// Let PostHog create anonymous ID
posthog.capture('app_opened');
}
identifyBasic(userId: string, email: string) {
// Basic identification after signup
posthog.alias(userId);
posthog.identify(userId, {
email: email
}, {
signup_date: new Date().toISOString()
});
}
enrichProfile(properties: Record<string, any>) {
// Add more properties as they become available
posthog.setPersonProperties(properties);
}
resetOnLogout() {
// Always reset on logout
posthog.reset();
}
}// Handle OAuth and multiple login methods
async function handleLogin(provider: 'google' | 'github' | 'email', credentials: any) {
const user = await authenticate(provider, credentials);
// Create alias from anonymous to identified
posthog.alias(user.id);
// Identify with provider info
posthog.identify(user.id, {
email: user.email,
name: user.name,
auth_provider: provider,
verified: user.verified
}, {
first_login_provider: provider,
first_login_date: new Date().toISOString()
});
// Reload flags for personalized experience
posthog.reloadFeatureFlags();
}// Implement opt-in by default for GDPR
posthog.init('token', {
opt_out_capturing_by_default: true,
opt_out_persistence_by_default: true,
respect_dnt: true
});
// Show consent banner
function showConsentBanner() {
// Display UI
const banner = createConsentBanner({
onAccept: () => {
posthog.opt_in_capturing({
captureEventName: 'gdpr_consent_granted',
captureProperties: {
consent_version: '2.0',
consent_date: new Date().toISOString()
}
});
// Enable features after consent
posthog.startSessionRecording();
},
onReject: () => {
posthog.opt_out_capturing();
}
});
}
// Check consent on load
if (posthog.get_explicit_consent_status() === 'pending') {
showConsentBanner();
}// Only capture necessary data
posthog.init('token', {
// Deny list for sensitive properties
property_denylist: [
'password',
'credit_card',
'ssn',
'api_key',
'access_token',
'secret'
],
// Mask personal data
mask_personal_data_properties: true,
custom_personal_data_properties: [
'email',
'phone',
'address',
'ip_address'
]
});// Lazy load PostHog for better initial page load
async function loadPostHogAsync() {
const { default: posthog } = await import('posthog-js');
posthog.init('token', {
loaded: (ph) => {
console.log('PostHog loaded asynchronously');
}
});
return posthog;
}
// Load after critical content
window.addEventListener('load', () => {
loadPostHogAsync();
});// Optimize for high-traffic pages
posthog.init('token', {
request_batching: true,
batch_size: 100, // Larger batches
batch_max_wait_ms: 10000, // Less frequent sends
disable_compression: false // Keep compression on
});// Only load features you need
posthog.init('token', {
// Disable unused features
disable_surveys: true,
disable_product_tours: true,
disable_conversations: true,
// Disable expensive features on low-end devices
session_recording: {
enabled: !isLowEndDevice(),
recordCanvas: false // Canvas recording is expensive
}
});
function isLowEndDevice(): boolean {
return (
navigator.deviceMemory && navigator.deviceMemory < 4 ||
navigator.hardwareConcurrency && navigator.hardwareConcurrency < 4
);
}// Set up global error handlers
class ErrorTracker {
initialize() {
// Uncaught errors
window.addEventListener('error', (event) => {
this.trackError(event.error, {
type: 'uncaught_error',
filename: event.filename,
lineno: event.lineno,
colno: event.colno
});
});
// Unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
this.trackError(event.reason, {
type: 'unhandled_rejection',
promise: String(event.promise)
});
});
// Enable PostHog exception autocapture
posthog.startExceptionAutocapture({
capture_unhandled_errors: true,
capture_unhandled_rejections: true,
capture_console_errors: false // We handle console separately
});
}
trackError(error: Error, context: Record<string, any>) {
// Deduplicate errors
const fingerprint = this.getErrorFingerprint(error);
if (this.recentErrors.has(fingerprint)) {
return;
}
this.recentErrors.add(fingerprint);
posthog.captureException(error, {
...context,
session_replay_url: posthog.get_session_replay_url({
withTimestamp: true,
timestampLookBack: 30
}),
user_id: posthog.get_distinct_id(),
session_id: posthog.get_session_id()
});
}
private recentErrors = new Set<string>();
private getErrorFingerprint(error: Error): string {
return `${error.name}:${error.message}:${error.stack?.split('\n')[1]}`;
}
}// Create PostHog mock for testing
const mockPostHog = {
init: jest.fn(),
capture: jest.fn(),
identify: jest.fn(),
isFeatureEnabled: jest.fn(() => false),
getFeatureFlag: jest.fn(() => undefined),
onFeatureFlags: jest.fn((cb) => {
cb({}, {});
return () => {};
})
};
// Use in tests
beforeEach(() => {
(window as any).posthog = mockPostHog;
});
afterEach(() => {
jest.clearAllMocks();
});
// Test with feature flags
it('shows new feature when flag is enabled', () => {
mockPostHog.isFeatureEnabled.mockReturnValue(true);
render(<Component />);
expect(screen.getByText('New Feature')).toBeInTheDocument();
});// Disable PostHog in E2E tests or use test environment
if (process.env.E2E_TEST) {
posthog.init('token', {
api_host: 'https://test.posthog.com',
// Or disable completely
opt_out_capturing_by_default: true
});
}
// Override flags for specific test scenarios
if (process.env.TEST_VARIANT === 'control') {
posthog.featureFlags.overrideFeatureFlags({
'experiment': 'control'
});
}// Monitor PostHog health
class PostHogMonitor {
checkHealth() {
return {
initialized: !!posthog,
capturing: posthog.is_capturing(),
flagsLoaded: posthog.featureFlags.hasLoadedFlags,
sessionRecording: posthog.sessionRecordingStarted(),
config: {
apiHost: posthog.config.api_host,
persistence: posthog.config.persistence
}
};
}
logHealthStatus() {
const health = this.checkHealth();
console.log('PostHog Health:', health);
if (!health.capturing) {
console.warn('PostHog not capturing events');
}
if (!health.flagsLoaded) {
console.warn('Feature flags not loaded');
}
}
}
// Check health on critical paths
async function initializeApp() {
const monitor = new PostHogMonitor();
// Wait for PostHog to be ready
await new Promise(resolve => setTimeout(resolve, 1000));
monitor.logHealthStatus();
// Continue with app initialization
}// Define event types
interface EventMap {
'button_clicked': {
button_id: string;
button_text: string;
location: string;
};
'page_viewed': {
page_path: string;
page_title: string;
};
'purchase_completed': {
order_id: string;
total: number;
currency: string;
items: Array<{
id: string;
quantity: number;
}>;
};
}
// Type-safe capture function
function captureEvent<K extends keyof EventMap>(
event: K,
properties: EventMap[K]
): void {
posthog.capture(event, properties);
}
// Usage with type checking
captureEvent('button_clicked', {
button_id: 'signup',
button_text: 'Sign Up',
location: 'header'
});
// ❌ Type error: missing required properties
// captureEvent('button_clicked', { button_id: 'signup' });// Define flag types
interface FeatureFlags {
'new-dashboard': boolean;
'button-color': 'blue' | 'green' | 'red';
'experiment-variant': 'control' | 'variant-a' | 'variant-b';
}
// Type-safe flag getter
function getFeatureFlag<K extends keyof FeatureFlags>(
key: K
): FeatureFlags[K] | undefined {
return posthog.getFeatureFlag(key) as FeatureFlags[K] | undefined;
}
// Usage with type inference
const buttonColor = getFeatureFlag('button-color'); // 'blue' | 'green' | 'red' | undefined
const isDashboardNew = getFeatureFlag('new-dashboard'); // boolean | undefined// ❌ Bad: Wait for PostHog before rendering
async function initApp() {
await initializePostHog(); // Don't block on this
render(<App />);
}
// ✅ Good: Initialize in parallel
function initApp() {
initializePostHog(); // Fire and forget
render(<App />);
}// ❌ Bad: Check flag immediately
function Component() {
const showNew = posthog.isFeatureEnabled('new-ui'); // May be undefined!
return showNew ? <NewUI /> : <OldUI />;
}
// ✅ Good: Handle loading state
function Component() {
const [showNew, setShowNew] = useState<boolean | null>(null);
useEffect(() => {
posthog.onFeatureFlags((flags) => {
setShowNew(flags['new-ui'] || false);
});
}, []);
if (showNew === null) return <Loading />;
return showNew ? <NewUI /> : <OldUI />;
}// ❌ Bad: Identify without alias (loses anonymous events)
function onSignup(userId: string) {
posthog.identify(userId);
}
// ✅ Good: Alias first to link anonymous events
function onSignup(userId: string) {
posthog.alias(userId); // Link anonymous ID to user ID
posthog.identify(userId, {
signup_date: new Date().toISOString()
});
}// ❌ Bad: Silently fail
try {
posthog.capture('event');
} catch (error) {
// Ignore
}
// ✅ Good: Log and monitor
try {
posthog.capture('event');
} catch (error) {
console.error('Analytics error:', error);
// Track to error monitoring service
errorMonitor.captureException(error);
}