TryGhost/Ghost

View on GitHub
ghost/admin/app/utils/analytics.js

Summary

Maintainability
A
1 hr
Test Coverage
// Wrapper function for Plausible event

function isPosthogLoaded() {
    return window.posthog?.__loaded;
}

/**
 * Hashes a user's email address so we can use it as a distinct_id in PostHog without storing the email address itself
 * 
 * 
 * @param {string} email an email address
 * @returns {(string|null)} a sha256 hash of the email address to use as distinct_id in PostHog — null if hashing fails
 */
async function hashEmail(email) {
    try {
        const digest = await window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(email.trim().toLowerCase()));
        const hashArray = Array.from(new Uint8Array(digest));
        const hash = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
        // Double-check that the hash is a valid sha256 hex string before returning it, else return null
        return hash.length === 64 ? hash : null;
    } catch (e) {
        // Modern browsers all support window.crypto, but we need to check for it to avoid errors on really old browsers
        // If any errors occur when hashing email, return null
        return null;
    }
}

/**
 * Sends a tracking event to Plausible and Posthog, if installed.
 * 
 * By default, Plausible and Posthog are not installed, in which case this function no-ops.
 * 
 * @param {string} eventName A string name for the event being tracked
 * @param {Object} [props={}] An optional object of properties to include with the event
 */
export function trackEvent(eventName, props = {}) {
    window.plausible = window.plausible || function () {
        (window.plausible.q = window.plausible.q || []).push(arguments);
    };
    window.plausible(eventName, {props: props});

    if (isPosthogLoaded()) {
        window.posthog.capture(eventName, props);
    }
}

/**
 * Calls posthog.identify() with a hashed email address as the distinct_id
 * 
 * @param {Object} user A user to identify in PostHog
 * @returns {void}
 */
export async function identifyUser(user) {
    // Return early if window.posthog doesn't exist
    if (!isPosthogLoaded()) {
        return;
    }
    // User the user exists and has an email address, identify them in PostHog
    if (user && user.get('email')) {
        const email = user.get('email');
        const hashedEmail = await hashEmail(email);
        const distinctId = window.posthog.get_distinct_id();
        // Only continue if hashing was successful, and the user hasn't already been identified
        if (hashedEmail && hashedEmail !== distinctId) {
            const props = {};
            // Add the user's id
            if (user.get('id')) {
                props.id = user.get('id');
            }
            // Add the user's role
            if (user.get('role').name) {
                props.role = user.get('role').name.toLowerCase();
            }
            // Add the user's created_at date
            if (user.get('createdAtUTC')) {
                props.created_at = user.get('createdAtUTC').toISOString();
            }
            window.posthog.identify(hashedEmail, props);
        }
    }
}

/**
 * Calls posthog.reset() to clear the current user's distinct_id and all associated properties
 * To be called when a user logs out
 * 
 * @returns {void}
 */
export function resetUser() {
    if (isPosthogLoaded()) {
        window.posthog.reset();
    }
}