UNPKG

unified-analytics

Version:

Unified analytics library for web applications

296 lines (295 loc) 10.8 kB
export * from "./types"; /** * Main Analytics class that implements the Observer pattern. * Acts as the central hub for tracking events across multiple analytics providers. */ class Analytics { constructor() { this._inHouseAttributes = {}; this._commonAttributes = {}; this._initialized = false; this._observers = new Set(); this._middleware = []; this._debug = false; // Load persisted attributes from sessionStorage if available const common = sessionStorage.getItem(Analytics.COMMON_ATTR_KEY); const inHouse = sessionStorage.getItem(Analytics.INHOUSE_ATTR_KEY); try { this._commonAttributes = common ? JSON.parse(common) : {}; } catch { this._commonAttributes = {}; } try { this._inHouseAttributes = inHouse ? JSON.parse(inHouse) : {}; } catch { this._inHouseAttributes = {}; } } /** * Initialize the analytics system with observers and common attributes. * Should be called once early in your application lifecycle. * * @param observers - Array of analytics providers that will receive events * @param options - Configuration options for the analytics system */ initialize(observers = [], options = {}) { if (this._initialized) { console.warn("Analytics already initialized. Skipping...\nTo modify providers, please use `attach` and `detach` methods"); return this; } this._debug = options.debug || false; observers.forEach((observer) => this.attach(observer)); this._initialized = true; return this; } /** * Register a new analytics provider to receive events. * * @param observer - The analytics provider to add */ attach(observer) { if (!this._observers.has(observer)) { this._observers.add(observer); } return this; } /** * Remove an analytics provider from receiving events. * * @param observer - The analytics provider to remove */ detach(observer) { this._observers.delete(observer); return this; } _notify(event) { if (this._debug) { console.log("%c Analytics Event ", "background: #2196F3; color: white; padding: 2px;", { name: event.name, attributes: event.attributes, observers: Array.from(this._observers).map((o) => o.constructor.name), }); } // Apply middleware chain if middleware exists if (this._middleware.length > 0) { this._applyMiddleware(event, 0); } else { // Send to observers directly if no middleware this._notifyObservers(event); } } _applyMiddleware(event, index) { if (index >= this._middleware.length) { // End of middleware chain, send to observers this._notifyObservers(event); return; } // Call current middleware with the next function this._middleware[index](event, (processedEvent) => { this._applyMiddleware(processedEvent, index + 1); }); } _applySingleObserverMiddleware(event, index, observer) { if (index >= this._middleware.length) { // End of middleware chain, send to the specific observer directly // (bypass the _notifySingleObserver method to avoid infinite recursion) setTimeout(() => observer.onEvent(event, this._debug, this._inHouseAttributes), 0); return; } // Call current middleware with the next function this._middleware[index](event, (processedEvent) => { this._applySingleObserverMiddleware(processedEvent, index + 1, observer); }); } _notifyObservers(event) { // Send each track action to background using `setTimeout` this._observers.forEach((observer) => setTimeout(() => observer.onEvent(event, this._debug, this._inHouseAttributes), 0)); } _notifySingleObserver(event, observer) { if (this._debug) { console.log("%c Analytics Event (Single Observer) ", "background: #2196F3; color: white; padding: 2px;", { name: event.name, attributes: event.attributes, observer: observer.constructor.name, }); } // Apply middleware chain if middleware exists if (this._middleware.length > 0) { this._applySingleObserverMiddleware(event, 0, observer); } else { // Send to specified observer directly if no middleware setTimeout(() => observer.onEvent(event, this._debug, this._inHouseAttributes), 0); } } /** * Track an analytics event and notify registered providers. * Common attributes will be automatically merged with event-specific attributes. * * @param eventName - Name of the event to track * @param attributes - Event-specific attributes to include * @param observers - Optional specific observer or array of observers to send the event to instead of all observers */ sendEvent(eventName, attributes, observers) { if (!this._initialized) { console.warn("Analytics not initialized. Event not tracked:", eventName); return this; } const event = { name: eventName, attributes: { ...this._commonAttributes, ...attributes }, }; if (observers) { if (Array.isArray(observers)) { // Send to multiple specific observers observers.forEach(observer => { this._notifySingleObserver(event, observer); }); } else { // Send to a single specific observer this._notifySingleObserver(event, observers); } } else { // Send to all observers this._notify(event); } return this; } /** * Update the common attributes that are included with every tracked event. * These will be merged with event-specific attributes. * * @param attributes - Common attributes to set or update */ setCommonAttributes(attributes) { this._commonAttributes = { ...this.getCommonAttributes(), ...attributes }; try { sessionStorage.setItem(Analytics.COMMON_ATTR_KEY, JSON.stringify(this._commonAttributes)); } catch { // If stringify fails, do not write to sessionStorage and keep previous values } return this; } /** * Get the current common attributes object. * @returns The common attributes */ getCommonAttributes() { const raw = sessionStorage.getItem(Analytics.COMMON_ATTR_KEY); if (!raw) return {}; try { return JSON.parse(raw); } catch { return {}; } } /** * Update the in-house attributes that are included with every tracked event for internal use. * These will be merged with existing in-house attributes. * @param attributes - In-house attributes to set or update * @returns The analytics instance for method chaining */ setInHouseAttributes(attributes) { this._inHouseAttributes = { ...this.getInHouseAttributes(), ...attributes }; try { sessionStorage.setItem(Analytics.INHOUSE_ATTR_KEY, JSON.stringify(this._inHouseAttributes)); } catch { // If stringify fails, do not write to sessionStorage and keep previous values } return this; } /** * Get the current in-house attributes object. * @returns The in-house attributes */ getInHouseAttributes() { const raw = sessionStorage.getItem(Analytics.INHOUSE_ATTR_KEY); if (!raw) return {}; try { return JSON.parse(raw); } catch { return {}; } } /** * Reset all analytics attributes, including both common and in-house attributes. * * @returns The analytics instance for method chaining */ resetAttributes() { this._commonAttributes = {}; this._inHouseAttributes = {}; sessionStorage.removeItem(Analytics.COMMON_ATTR_KEY); sessionStorage.removeItem(Analytics.INHOUSE_ATTR_KEY); return this; } /** * Enable or disable debug mode at runtime * @param value - True to enable debug mode, false to disable */ setDebug(value) { this._debug = value; return this; } /** * Check if analytics has been initialized. * * @returns True if analytics has been initialized, false otherwise */ isInitialized() { return this._initialized; } /** * Creates a clone of an existing analytics provider. * This is useful when you need multiple instances of the same provider * with different configurations. * * @param observer - The original observer to clone * @param options - Options for cloning the provider * @param options.autoAttach - When true, automatically attaches the cloned provider to the analytics service * @returns The cloned observer instance * * @example * // Clone Google Analytics and automatically attach it to analytics * const newGAService = analytics.cloneProvider(firebaseGoogleAnalyticsService, { autoAttach: true }); * newGAService.init({ ...different config... }); * * // Or clone without auto-attaching (original behavior) * const anotherService = analytics.cloneProvider(someProvider); * anotherService.init({ ...different config... }); * analytics.attach(anotherService); */ cloneProvider(observer, options) { const cloned = observer.clone(); if (options?.autoAttach) { this.attach(cloned); } return cloned; } /** * Register a middleware function that will process events before they are sent to observers. * Middleware functions are executed in the order they are registered. * * @param middleware - Function that receives the event and a next callback * @returns The analytics instance for method chaining */ use(middleware) { this._middleware.push(middleware); return this; } } Analytics.COMMON_ATTR_KEY = "ua_common_attributes"; Analytics.INHOUSE_ATTR_KEY = "ua_inhouse_attributes"; const analytics = new Analytics(); export default analytics;