@unifygtm/intent-client
Version:
JavaScript client for interacting with the Unify Intent API in the browser.
510 lines (449 loc) • 16.5 kB
text/typescript
import type {
AnalyticsEventBase,
AutoTrackOptions,
PageEventOptions,
TrackEventData,
TrackEventProperties,
UCompany,
UnifyIntentClientConfig,
UnifyIntentContext,
UPerson,
} from '../types';
import { IdentifyActivity, PageActivity, TrackActivity } from './activities';
import { IdentityManager, SessionManager } from './managers';
import UnifyApiClient from './unify-api-client';
import { UnifyIntentAgent } from './agent';
import { isIntentClient, validateEmail } from './utils/helpers';
import { logUnifyError } from './utils/logging';
import {
DEFAULT_AUTO_TRACK_OPTIONS,
DEFAULT_SESSION_MINUTES_TO_EXPIRE,
} from './constants';
declare global {
interface Window {
unify?: UnifyIntentClient;
unifyBrowser?: UnifyIntentClient;
}
}
export const DEFAULT_UNIFY_INTENT_CLIENT_CONFIG: UnifyIntentClientConfig = {
autoPage: false,
autoIdentify: false,
sessionDurationMinutes: DEFAULT_SESSION_MINUTES_TO_EXPIRE,
autoTrackOptions: DEFAULT_AUTO_TRACK_OPTIONS,
};
type EventBuffers = {
identify: [string, { person?: UPerson; company?: UCompany } | undefined][];
page: [PageEventOptions | undefined][];
track: [string, TrackEventProperties | undefined][];
};
function getEmptyBuffers(): EventBuffers {
return {
identify: [],
page: [],
track: [],
};
}
/**
* This class is used to leverage the Unify Intent API to log user
* analytics like page views, sessions, identity, and actions.
*/
export default class UnifyIntentClient {
private readonly _writeKey: string;
private readonly _config: UnifyIntentClientConfig;
private _mounted: boolean = false;
private _context!: UnifyIntentContext;
private _intentAgent?: UnifyIntentAgent;
private _eventBuffers: EventBuffers = getEmptyBuffers();
constructor(
writeKey: string,
config: UnifyIntentClientConfig = DEFAULT_UNIFY_INTENT_CLIENT_CONFIG,
) {
this._writeKey = writeKey;
this._config = config;
}
/**
* Getter to check if the intent client is currently mounted or not.
*
* @returns `true` if the client is mounted, otherwise `false`
*/
public isMounted = () => {
return this._mounted;
};
/**
* This function initializes the `UnifyIntentClient` for use. It should only
* be called once the global `window` object exists. If using the client in
* React, for example, this would take place inside a `useEffect` hook.
*/
public mount = () => {
// The client should never be initialized outside a global window context
if (typeof window === 'undefined') return;
try {
// The client should never be instantiated more than once
if (isIntentClient(window.unify) || isIntentClient(window.unifyBrowser)) {
logUnifyError({
message:
'UnifyIntentClient already exists on window, a new one will not be created.',
});
return;
}
// Initialize API client
const apiClient = new UnifyApiClient(this._writeKey);
// Initialize user session
const sessionManager = new SessionManager(this._writeKey, {
durationMinutes: this._config.sessionDurationMinutes,
});
sessionManager.getOrCreateSession();
// Create visitor ID if needed
const identityManager = new IdentityManager(this._writeKey);
identityManager.getOrCreateVisitorId();
// Initialize context
this._context = {
writeKey: this._writeKey,
clientConfig: this._config,
apiClient,
sessionManager,
identityManager,
};
// Initialize intent agent if specifed by config
this._intentAgent = new UnifyIntentAgent(this._context);
// We set `mounted` to `true` before flushing the queue since the
// methdods which can be called require that.
this._mounted = true;
// When the client is loaded from CDN, it's possible that method
// calls have been queued on `window.unify`
flushUnifyQueue(this, apiClient);
// Fire any buffered events from before the client was mounted
this._eventBuffers.identify.forEach((args) => this.identify(...args));
this._eventBuffers.page.forEach((args) => this.page(...args));
this._eventBuffers.track.forEach((args) => this.track(...args));
// Reset buffered events
this._eventBuffers = getEmptyBuffers();
// Set unify object on window to prevent multiple instantiations
window.unify = this;
window.unifyBrowser = this;
} catch (error: unknown) {
this.logError('Error occurred in mount', error);
}
};
/**
* This function cleans up this instance of the `UnifyIntentClient` when
* it should be unmounted from the DOM. If using the client in
* React, for example, this would take place inside the function returned
* by the same `useEffect` used to mount the client.
*/
public unmount = () => {
// If window no longer exists at this point, there is nothing to unmount
if (typeof window === 'undefined') return;
try {
if (this._config.autoPage) {
this.stopAutoPage();
}
if (this._config.autoIdentify) {
this.stopAutoIdentify();
}
this.stopAutoTrack();
this._intentAgent?.unmount();
this._mounted = false;
window.unify = undefined;
window.unifyBrowser = undefined;
} catch (error: unknown) {
this.logError('Error occurred in unmount', error);
}
};
/**
* This function logs a page view for the current page or the page
* specified in options to the Unify Intent API.
*
* @param options - options which can be used to customize the page event which is logged. See `PageEventOptions` for details.
*/
public page = (options?: PageEventOptions) => {
if (!this._mounted) {
this._eventBuffers.page.push([options]);
return;
}
try {
const action = new PageActivity(this._context, options);
action.track();
} catch (error: unknown) {
this.logError('Error occurred in page', error);
}
};
/**
* This function returns the request payload for tracking a page event.
* This is useful if you want to send the payload to a proxy server to
* perform the tracking server-side.
*
* @param options - options which can be used to customize the page event which is tracked. See `PageEventOptions` for details.
* @returns if the client is mounted, the request payload to track a page event, otherwise returns `undefined`
*/
public getPagePayload = (options?: PageEventOptions) => {
if (!this._mounted) return;
try {
const action = new PageActivity(this._context, options);
return action.getTrackPayload();
} catch (error: unknown) {
this.logError('Error occurred in getPagePayload', error);
}
};
/**
* This function logs an identify event for the given email address
* to the Unify Intent API. Unify will associate this email address
* with the current user's session and all related activities.
*
* @param email - the email address to log an identify event for
* @param options - object containing Person or Company data to associate with the identified visitor. If the Person or Company already exists in Unify, they will be updated, otherwise they will be created.
* @returns `true` if the email was valid and logged, otherwise `false`
*/
public identify = (
email: string,
options?: { person?: UPerson; company?: UCompany },
): boolean => {
try {
const validatedEmail = validateEmail(email);
if (validatedEmail) {
if (!this._mounted) {
this._eventBuffers.identify.push([email, options]);
return true;
}
const action = new IdentifyActivity(this._context, {
email: validatedEmail,
person: options?.person,
company: options?.company,
});
action.track();
return true;
}
} catch (error: unknown) {
this.logError('Error occurred in identify', error);
}
return false;
};
/**
* This function returns the request payload for tracking an identify event.
* This is useful if you want to send the payload to a proxy server to
* perform the tracking server-side.
*
* @param email - the email address to log an identify event for
* @param options - object containing Person or Company data to associate with the identified visitor. If the Person or Company already exists in Unify, they will be updated, otherwise they will be created.
* @returns if the client is mounted and `email` is a valid email address, the request payload to track an identify event, otherwise returns `undefined`
*/
public getIdentifyPayload = (
email: string,
options?: { person?: UPerson; company?: UCompany },
) => {
if (!this._mounted) return false;
try {
const validatedEmail = validateEmail(email);
if (validatedEmail) {
const action = new IdentifyActivity(this._context, {
email: validatedEmail,
person: options?.person,
company: options?.company,
});
return action.getTrackPayload();
}
} catch (error: unknown) {
this.logError('Error occurred in getIdentifyPayload', error);
}
};
/**
* This function logs a track event with the given name and properties
* to the Unify Intent API. Unify will associate this event
* with the current user's session and all related activities.
*
* @param name - the name of the event to track, e.g. "Demo Button Clicked"
* @param properties - optional properties to associate with the event
*/
public track = (name: string, properties?: TrackEventProperties): void => {
if (!this._mounted) {
this._eventBuffers.track.push([name, properties]);
return;
}
try {
const action = new TrackActivity(this._context, { name, properties });
action.track();
} catch (error: unknown) {
this.logError('Error occurred in track', error);
}
};
/**
* This function returns the request payload for a single track event.
* This is useful if you want to send the payload to a proxy server to
* perform the tracking server-side.
*
* @param name - the name of the event to track, e.g. "Demo Button Clicked"
* @param properties - optional properties to associate with the event
* @returns if the client is mounted, the request payload for a track event, otherwise `undefined`
*/
public getTrackPayload = (
name: string,
properties: TrackEventProperties,
): (AnalyticsEventBase & TrackEventData) | undefined => {
if (!this._mounted) return;
try {
const action = new TrackActivity(this._context, { name, properties });
return action.getTrackPayload();
} catch (error: unknown) {
this.logError('Error occurred in getTrackPayload', error);
}
};
/**
* This function will instantiate an agent which continuously monitors
* page changes to automatically log page events.
*
* The corresponding `stopAutoPage` can be used to temporarily
* stop the continuous monitoring.
*/
public startAutoPage = () => {
if (!this._mounted) return;
try {
if (!this._intentAgent) {
this._intentAgent = new UnifyIntentAgent(this._context);
}
this._intentAgent.startAutoPage();
} catch (error: unknown) {
this.logError('Error occurred in startAutoPage', error);
}
};
/**
* If continuous page monitoring was previously triggered, this function
* is used to halt the monitoring.
*
* The corresponding `startAutoPage` can be used to start it again.
*/
public stopAutoPage = () => {
if (!this._mounted) return;
try {
this._intentAgent?.stopAutoPage();
} catch (error: unknown) {
this.logError('Error occurred in stopAutoPage', error);
}
};
/**
* This function will instantiate an agent which continuously monitors
* input elements on the page to automatically log user self-identification.
*
* The corresponding `stopAutoIdentify` can be used to temporarily
* stop the continuous monitoring.
*/
public startAutoIdentify = () => {
if (!this._mounted) return;
try {
if (!this._intentAgent) {
this._intentAgent = new UnifyIntentAgent(this._context);
}
this._intentAgent.startAutoIdentify();
} catch (error: unknown) {
this.logError('Error occurred in startAutoIdentify', error);
}
};
/**
* If continuous input monitoring was previously triggered, this function
* is used to halt the monitoring.
*
* The corresponding `startAutoIdentify` can be used to start it again.
*/
public stopAutoIdentify = () => {
if (!this._mounted) return;
try {
this._intentAgent?.stopAutoIdentify();
} catch (error: unknown) {
this.logError('Error occurred in stopAutoIdentify', error);
}
};
/**
* This function will instantiate an agent which continuously monitors
* user actions on the page to automatically fire track events for them.
*
* The corresponding `stopAutoTrack` can be used to temporarily
* stop the continuous monitoring.
*
* @param options - auto-track options to use. If `undefined`, the previously
* specified auto-track options will be re-used.
*/
public startAutoTrack = (options?: AutoTrackOptions) => {
if (!this._mounted) return;
try {
if (options) {
this._config.autoTrackOptions = options;
}
if (!this._intentAgent) {
this._intentAgent = new UnifyIntentAgent(this._context);
}
this._intentAgent.startAutoTrack(options);
} catch (error: unknown) {
this.logError('Error occurred in startAutoTrack', error);
}
};
/**
* If continuous user action monitoring was previously enabled, this function
* is used to halt the monitoring.
*
* The corresponding `startAutoTrack` can be used to start it again.
*/
public stopAutoTrack = () => {
if (!this._mounted) return;
try {
this._intentAgent?.stopAutoTrack();
} catch (error: unknown) {
this.logError('Error occurred in stopAutoTrack', error);
}
};
private logError = (message: string, error: unknown) => {
logUnifyError({
message: `UnifyIntentClient: ${message}`,
error: error as Error,
apiClient: this._context.apiClient,
});
};
/**
* DO NOT USE: These methods are exposed only for testing purposes.
*/
__getEventBuffers = () => this._eventBuffers;
}
/**
* It's possible that client code will execute functions on the global
* `UnifyIntentClient` object before it is actually loaded and instantiated
* because this code is loaded asynchronously by the client.
*
* Until `flushUnifyQueue` is called, `window.unify` is set to an array of queued
* method calls, which are themselves each represented by an array. The first
* element of each of these "method call subarrays" is the method name to call,
* and the rest of the elements are the arguments to pass to that method.
*
* Once the Unify intent script is loaded and the `UnifyIntentClient` has
* been instantiated, this function is called to flush the queued method
* calls on the existing `window.unify` array if there are any to flush. It
* iterates over each queued method call and applies that method and its
* arguments to the newly instantiated `UnifyIntentClient`.
*
* @param unify - the `UnifyIntentClient` to apply method calls to
*/
function flushUnifyQueue(unify: UnifyIntentClient, apiClient: UnifyApiClient) {
const queue = Array.isArray(window.unify)
? [...window.unify]
: Array.isArray(window.unifyBrowser)
? [...window.unifyBrowser]
: [];
queue.forEach(([method, args]) => {
if (typeof unify[method as keyof UnifyIntentClient] === 'function') {
try {
if (Array.isArray(args)) {
// @ts-ignore the type of the args is unknown at this point
unify[method as keyof UnifyIntentClient].call(unify, ...args);
} else {
// @ts-ignore the type of the args is unknown at this point
unify[method as keyof UnifyIntentClient].call(unify);
}
} catch (error: any) {
// Swallow errors so client is not potentially affected, this
// should ideally never happen.
logUnifyError({
message: `Error occurred while flushing queue: ${error?.message}`,
error: error as Error,
apiClient,
});
}
}
});
}