unified-analytics
Version:
Unified analytics library for web applications
296 lines (295 loc) • 10.8 kB
JavaScript
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;