libraries/tracker-core/src/core.ts
/*
* Copyright (c) 2022 Snowplow Analytics Ltd, 2010 Anthon Pang
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import { v4 as uuid } from 'uuid';
import { payloadBuilder, PayloadBuilder, Payload, isJson, payloadJsonProcessor } from './payload';
import {
globalContexts,
ConditionalContextProvider,
ContextPrimitive,
GlobalContexts,
PluginContexts,
pluginContexts,
} from './contexts';
import { CorePlugin } from './plugins';
import { LOG } from './logger';
/**
* Export interface for any Self-Describing JSON such as context or Self Describing events
* @typeParam T - The type of the data object within a SelfDescribingJson
*/
export type SelfDescribingJson<T = Record<string, unknown>> = {
/**
* The schema string
* @example 'iglu:com.snowplowanalytics.snowplow/web_page/jsonschema/1-0-0'
*/
schema: string;
/**
* The data object which should conform to the supplied schema
*/
data: T extends any[] ? never : T extends {} ? T : never;
};
/**
* Export interface for any Self-Describing JSON which has the data attribute as an array
* @typeParam T - The type of the data object within the SelfDescribingJson data array
*/
export type SelfDescribingJsonArray<T = Record<string, unknown>> = {
/**
* The schema string
* @example 'iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-1'
*/
schema: string;
/**
* The data array which should conform to the supplied schema
*/
data: (T extends SelfDescribingJson ? T : SelfDescribingJson<T>)[];
};
/**
* Algebraic datatype representing possible timestamp type choice
*/
export type Timestamp = TrueTimestamp | DeviceTimestamp | number;
/**
* A representation of a True Timestamp (ttm)
*/
export interface TrueTimestamp {
readonly type: 'ttm';
readonly value: number;
}
/**
* A representation of a Device Timestamp (dtm)
*/
export interface DeviceTimestamp {
readonly type: 'dtm';
readonly value: number;
}
/**
* Pair of timestamp type ready to be included to payload
*/
type TimestampPayload = TrueTimestamp | DeviceTimestamp;
/**
* Transform optional/old-behavior number timestamp into`Timestamp` ADT
*
* @param timestamp - optional number or timestamp object
* @returns correct timestamp object
*/
function getTimestamp(timestamp?: Timestamp | null): TimestampPayload {
if (timestamp == null) {
return { type: 'dtm', value: new Date().getTime() };
} else if (typeof timestamp === 'number') {
return { type: 'dtm', value: timestamp };
} else if (timestamp.type === 'ttm') {
// We can return timestamp here, but this is safer fallback
return { type: 'ttm', value: timestamp.value };
} else {
return { type: 'dtm', value: timestamp.value || new Date().getTime() };
}
}
/** Additional data points to set when tracking an event */
export interface CommonEventProperties<T = Record<string, unknown>> {
/** Add context to an event by setting an Array of Self Describing JSON */
context?: Array<SelfDescribingJson<T>> | null;
/** Set the true timestamp or overwrite the device sent timestamp on an event */
timestamp?: Timestamp | null;
}
/**
* Export interface containing all Core functions
*/
export interface TrackerCore {
/**
* Call with a payload from a buildX function
* Adds context and payloadPairs name-value pairs to the payload
* Applies the callback to the built payload
*
* @param pb - Payload
* @param context - Custom contexts relating to the event
* @param timestamp - Timestamp of the event
* @returns Payload after the callback is applied or undefined if the event is skipped
*/
track: (
/** A PayloadBuilder created by one of the `buildX` functions */
pb: PayloadBuilder,
/** The additional contextual information related to the event */
context?: Array<SelfDescribingJson> | null,
/** Timestamp override */
timestamp?: Timestamp | null
) => Payload | undefined;
/**
* Set a persistent key-value pair to be added to every payload
*
* @param key - Field name
* @param value - Field value
*/
addPayloadPair: (key: string, value: unknown) => void;
/**
* Get current base64 encoding state
*/
getBase64Encoding(): boolean;
/**
* Turn base 64 encoding on or off
*
* @param encode - Whether to encode payload
*/
setBase64Encoding(encode: boolean): void;
/**
* Merges a dictionary into payloadPairs
*
* @param dict - Adds a new payload dictionary to the existing one
*/
addPayloadDict(dict: Payload): void;
/**
* Replace payloadPairs with a new dictionary
*
* @param dict - Resets all current payload pairs with a new dictionary of pairs
*/
resetPayloadPairs(dict: Payload): void;
/**
* Set the tracker version
*
* @param version - The version of the current tracker
*/
setTrackerVersion(version: string): void;
/**
* Set the tracker namespace
*
* @param name - The trackers namespace
*/
setTrackerNamespace(name: string): void;
/**
* Set the application ID
*
* @param appId - An application ID which identifies the current application
*/
setAppId(appId: string): void;
/**
* Set the platform
*
* @param value - A valid Snowplow platform value
*/
setPlatform(value: string): void;
/**
* Set the user ID
*
* @param userId - The custom user id
*/
setUserId(userId: string): void;
/**
* Set the screen resolution
*
* @param width - screen resolution width
* @param height - screen resolution height
*/
setScreenResolution(width: string, height: string): void;
/**
* Set the viewport dimensions
*
* @param width - viewport width
* @param height - viewport height
*/
setViewport(width: string, height: string): void;
/**
* Set the color depth
*
* @param depth - A color depth value as string
*/
setColorDepth(depth: string): void;
/**
* Set the timezone
*
* @param timezone - A timezone string
*/
setTimezone(timezone: string): void;
/**
* Set the language
*
* @param lang - A language string e.g. 'en-UK'
*/
setLang(lang: string): void;
/**
* Set the IP address
*
* @param ip - An IP Address string
*/
setIpAddress(ip: string): void;
/**
* Set the Useragent
*
* @param useragent - A useragent string
*/
setUseragent(useragent: string): void;
/**
* Adds contexts globally, contexts added here will be attached to all applicable events
* @param contexts - An array containing either contexts or a conditional contexts
*/
addGlobalContexts(
contexts:
| Array<ConditionalContextProvider | ContextPrimitive>
| Record<string, ConditionalContextProvider | ContextPrimitive>
): void;
/**
* Removes all global contexts
*/
clearGlobalContexts(): void;
/**
* Removes previously added global context, performs a deep comparison of the contexts or conditional contexts
* @param contexts - An array containing either contexts or a conditional contexts
*/
removeGlobalContexts(contexts: Array<ConditionalContextProvider | ContextPrimitive | string>): void;
/**
* Add a plugin into the plugin collection after Core has already been initialised
* @param configuration - The plugin to add
*/
addPlugin(configuration: CorePluginConfiguration): void;
}
/**
* The configuration object for the tracker core library
*/
export interface CoreConfiguration {
/* Should payloads be base64 encoded when built */
base64?: boolean;
/* A list of all the plugins to include at load */
corePlugins?: Array<CorePlugin>;
/* The callback which will fire each time `track()` is called */
callback?: (PayloadData: PayloadBuilder) => void;
}
/**
* The configuration of the plugin to add
*/
export interface CorePluginConfiguration {
/* The plugin to add */
plugin: CorePlugin;
}
/**
* Create a tracker core object
*
* @param base64 - Whether to base 64 encode contexts and self describing event JSONs
* @param corePlugins - The core plugins to be processed with each event
* @param callback - Function applied to every payload dictionary object
* @returns Tracker core
*/
export function trackerCore(configuration: CoreConfiguration = {}): TrackerCore {
function newCore(base64: boolean, corePlugins: Array<CorePlugin>, callback?: (PayloadData: PayloadBuilder) => void) {
const pluginContextsHelper: PluginContexts = pluginContexts(corePlugins),
globalContextsHelper: GlobalContexts = globalContexts();
let encodeBase64 = base64,
payloadPairs: Payload = {}; // Dictionary of key-value pairs which get added to every payload, e.g. tracker version
/**
* Wraps an array of custom contexts in a self-describing JSON
*
* @param contexts - Array of custom context self-describing JSONs
* @returns Outer JSON
*/
function completeContexts(
contexts?: Array<SelfDescribingJson>
): SelfDescribingJsonArray<SelfDescribingJson> | undefined {
if (contexts && contexts.length) {
return {
schema: 'iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-0',
data: contexts,
};
}
return undefined;
}
/**
* Adds all global contexts to a contexts array
*
* @param pb - PayloadData
* @param contexts - Custom contexts relating to the event
*/
function attachGlobalContexts(
pb: PayloadBuilder,
contexts?: Array<SelfDescribingJson> | null
): Array<SelfDescribingJson> {
const applicableContexts: Array<SelfDescribingJson> = globalContextsHelper.getApplicableContexts(pb);
const returnedContexts: Array<SelfDescribingJson> = [];
if (contexts && contexts.length) {
returnedContexts.push(...contexts);
}
if (applicableContexts && applicableContexts.length) {
returnedContexts.push(...applicableContexts);
}
return returnedContexts;
}
/**
* Gets called by every trackXXX method
* Adds context and payloadPairs name-value pairs to the payload
* Applies the callback to the built payload
*
* @param pb - Payload
* @param context - Custom contexts relating to the event
* @param timestamp - Timestamp of the event
* @returns Payload after the callback is applied or undefined if the event is skipped
*/
function track<C = Record<string, unknown>>(
pb: PayloadBuilder,
context?: Array<SelfDescribingJson<C>> | null,
timestamp?: Timestamp | null
): Payload | undefined {
pb.withJsonProcessor(payloadJsonProcessor(encodeBase64));
pb.add('eid', uuid());
pb.addDict(payloadPairs);
const tstamp = getTimestamp(timestamp);
pb.add(tstamp.type, tstamp.value.toString());
const allContexts = attachGlobalContexts(pb, pluginContextsHelper.addPluginContexts(context));
const wrappedContexts = completeContexts(allContexts);
if (wrappedContexts !== undefined) {
pb.addJson('cx', 'co', wrappedContexts);
}
corePlugins.forEach((plugin) => {
try {
if (plugin.beforeTrack) {
plugin.beforeTrack(pb);
}
} catch (ex) {
LOG.error('Plugin beforeTrack', ex);
}
});
// Call the filter on plugins to determine if the event should be tracked
const skip = corePlugins.find((plugin) => {
try {
return plugin.filter && plugin.filter(pb.build()) === false;
} catch (ex) {
LOG.error('Plugin filter', ex);
return false;
}
});
if (skip) {
return undefined;
}
if (typeof callback === 'function') {
callback(pb);
}
const finalPayload = pb.build();
corePlugins.forEach((plugin) => {
try {
if (plugin.afterTrack) {
plugin.afterTrack(finalPayload);
}
} catch (ex) {
LOG.error('Plugin afterTrack', ex);
}
});
return finalPayload;
}
/**
* Set a persistent key-value pair to be added to every payload
*
* @param key - Field name
* @param value - Field value
*/
function addPayloadPair(key: string, value: unknown): void {
payloadPairs[key] = value;
}
const core = {
track,
addPayloadPair,
getBase64Encoding() {
return encodeBase64;
},
setBase64Encoding(encode: boolean) {
encodeBase64 = encode;
},
addPayloadDict(dict: Payload) {
for (const key in dict) {
if (Object.prototype.hasOwnProperty.call(dict, key)) {
payloadPairs[key] = dict[key];
}
}
},
resetPayloadPairs(dict: Payload) {
payloadPairs = isJson(dict) ? dict : {};
},
setTrackerVersion(version: string) {
addPayloadPair('tv', version);
},
setTrackerNamespace(name: string) {
addPayloadPair('tna', name);
},
setAppId(appId: string) {
addPayloadPair('aid', appId);
},
setPlatform(value: string) {
addPayloadPair('p', value);
},
setUserId(userId: string) {
addPayloadPair('uid', userId);
},
setScreenResolution(width: string, height: string) {
addPayloadPair('res', width + 'x' + height);
},
setViewport(width: string, height: string) {
addPayloadPair('vp', width + 'x' + height);
},
setColorDepth(depth: string) {
addPayloadPair('cd', depth);
},
setTimezone(timezone: string) {
addPayloadPair('tz', timezone);
},
setLang(lang: string) {
addPayloadPair('lang', lang);
},
setIpAddress(ip: string) {
addPayloadPair('ip', ip);
},
setUseragent(useragent: string) {
addPayloadPair('ua', useragent);
},
addGlobalContexts(contexts: Array<ConditionalContextProvider | ContextPrimitive>) {
globalContextsHelper.addGlobalContexts(contexts);
},
clearGlobalContexts(): void {
globalContextsHelper.clearGlobalContexts();
},
removeGlobalContexts(contexts: Array<ConditionalContextProvider | ContextPrimitive>) {
globalContextsHelper.removeGlobalContexts(contexts);
},
};
return core;
}
const { base64, corePlugins, callback } = configuration,
plugins = corePlugins ?? [],
partialCore = newCore(base64 ?? true, plugins, callback),
core = {
...partialCore,
addPlugin: (configuration: CorePluginConfiguration) => {
const { plugin } = configuration;
plugins.push(plugin);
plugin.logger?.(LOG);
plugin.activateCorePlugin?.(core);
},
};
plugins?.forEach((plugin) => {
plugin.logger?.(LOG);
plugin.activateCorePlugin?.(core);
});
return core;
}
/**
* A Self Describing Event
* A custom event type, allowing for an event to be tracked using your own custom schema
* and a data object which conforms to the supplied schema
*/
export interface SelfDescribingEvent<T = Record<string, unknown>> {
/** The Self Describing JSON which describes the event */
event: SelfDescribingJson<T>;
}
/**
* Build a self-describing event
* A custom event type, allowing for an event to be tracked using your own custom schema
* and a data object which conforms to the supplied schema
*
* @param event - Contains the properties and schema location for the event
* @returns PayloadBuilder to be sent to {@link @snowplow/tracker-core#TrackerCore.track}
*/
export function buildSelfDescribingEvent<T = Record<string, unknown>>(event: SelfDescribingEvent<T>): PayloadBuilder {
const {
event: { schema, data },
} = event,
pb = payloadBuilder();
const ueJson = {
schema: 'iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0',
data: { schema, data },
};
pb.add('e', 'ue');
pb.addJson('ue_px', 'ue_pr', ueJson);
return pb;
}
/**
* A Page View Event
* Represents a Page View, which is typically fired as soon as possible when a web page
* is loaded within the users browser. Often also fired on "virtual page views" within
* Single Page Applications (SPA).
*/
export interface PageViewEvent {
/** The current URL visible in the users browser */
pageUrl?: string | null;
/** The current page title in the users browser */
pageTitle?: string | null;
/** The URL of the referring page */
referrer?: string | null;
}
/**
* Build a Page View Event
* Represents a Page View, which is typically fired as soon as possible when a web page
* is loaded within the users browser. Often also fired on "virtual page views" within
* Single Page Applications (SPA).
*
* @param event - Contains the properties for the Page View event
* @returns PayloadBuilder to be sent to {@link @snowplow/tracker-core#TrackerCore.track}
*/
export function buildPageView(event: PageViewEvent): PayloadBuilder {
const { pageUrl, pageTitle, referrer } = event,
pb = payloadBuilder();
pb.add('e', 'pv'); // 'pv' for Page View
pb.add('url', pageUrl);
pb.add('page', pageTitle);
pb.add('refr', referrer);
return pb;
}
/**
* A Page Ping Event
* Fires when activity tracking is enabled in the browser.
* Tracks same information as the last tracked Page View and includes scroll
* information from the current page view
*/
export interface PagePingEvent extends PageViewEvent {
/** The minimum X scroll position for the current page view */
minXOffset?: number;
/** The maximum X scroll position for the current page view */
maxXOffset?: number;
/** The minimum Y scroll position for the current page view */
minYOffset?: number;
/** The maximum Y scroll position for the current page view */
maxYOffset?: number;
}
/**
* Build a Page Ping Event
* Fires when activity tracking is enabled in the browser.
* Tracks same information as the last tracked Page View and includes scroll
* information from the current page view
*
* @param event - Contains the properties for the Page Ping event
* @returns PayloadBuilder to be sent to {@link @snowplow/tracker-core#TrackerCore.track}
*/
export function buildPagePing(event: PagePingEvent): PayloadBuilder {
const { pageUrl, pageTitle, referrer, minXOffset, maxXOffset, minYOffset, maxYOffset } = event,
pb = payloadBuilder();
pb.add('e', 'pp'); // 'pp' for Page Ping
pb.add('url', pageUrl);
pb.add('page', pageTitle);
pb.add('refr', referrer);
if (minXOffset && !isNaN(Number(minXOffset))) pb.add('pp_mix', minXOffset.toString());
if (maxXOffset && !isNaN(Number(maxXOffset))) pb.add('pp_max', maxXOffset.toString());
if (minYOffset && !isNaN(Number(minYOffset))) pb.add('pp_miy', minYOffset.toString());
if (maxYOffset && !isNaN(Number(maxYOffset))) pb.add('pp_may', maxYOffset.toString());
return pb;
}
/**
* A Structured Event
* A classic style of event tracking, allows for easier movement between analytics
* systems. A loosely typed event, creating a Self Describing event is preferred, but
* useful for interoperability.
*/
export interface StructuredEvent {
category: string;
action: string;
label?: string;
property?: string;
value?: number;
}
/**
* Build a Structured Event
* A classic style of event tracking, allows for easier movement between analytics
* systems. A loosely typed event, creating a Self Describing event is preferred, but
* useful for interoperability.
*
* @param event - Contains the properties for the Structured event
* @returns PayloadBuilder to be sent to {@link @snowplow/tracker-core#TrackerCore.track}
*/
export function buildStructEvent(event: StructuredEvent): PayloadBuilder {
const { category, action, label, property, value } = event,
pb = payloadBuilder();
pb.add('e', 'se'); // 'se' for Structured Event
pb.add('se_ca', category);
pb.add('se_ac', action);
pb.add('se_la', label);
pb.add('se_pr', property);
pb.add('se_va', value == null ? undefined : value.toString());
return pb;
}
/**
* An Ecommerce Transaction Event
* Allows for tracking common ecommerce events, this event is usually used when
* a customer completes a transaction.
*/
export interface EcommerceTransactionEvent {
/** An identifier for the order */
orderId: string;
/** The total value of the order */
total: number;
/** Transaction affiliation (e.g. store where sale took place) */
affiliation?: string;
/** The amount of tax on the transaction */
tax?: number;
/** The amount of shipping costs for this transaction */
shipping?: number;
/** Delivery address, city */
city?: string;
/** Delivery address, state */
state?: string;
/** Delivery address, country */
country?: string;
/** Currency of the transaction */
currency?: string;
}
/**
* Build an Ecommerce Transaction Event
* Allows for tracking common ecommerce events, this event is usually used when
* a consumer completes a transaction.
*
* @param event - Contains the properties for the Ecommerce Transactoion event
* @returns PayloadBuilder to be sent to {@link @snowplow/tracker-core#TrackerCore.track}
*/
export function buildEcommerceTransaction(event: EcommerceTransactionEvent): PayloadBuilder {
const { orderId, total, affiliation, tax, shipping, city, state, country, currency } = event,
pb = payloadBuilder();
pb.add('e', 'tr'); // 'tr' for Transaction
pb.add('tr_id', orderId);
pb.add('tr_af', affiliation);
pb.add('tr_tt', total);
pb.add('tr_tx', tax);
pb.add('tr_sh', shipping);
pb.add('tr_ci', city);
pb.add('tr_st', state);
pb.add('tr_co', country);
pb.add('tr_cu', currency);
return pb;
}
/**
* An Ecommerce Transaction Item
* Related to the {@link EcommerceTransactionEvent}
* Each Ecommerce Transaction may contain one or more EcommerceTransactionItem events
*/
export interface EcommerceTransactionItemEvent {
/** An identifier for the order */
orderId: string;
/** A Product Stock Keeping Unit (SKU) */
sku: string;
/** The price of the product */
price: number;
/** The name of the product */
name?: string;
/** The category the product belongs to */
category?: string;
/** The quanity of this product within the transaction */
quantity?: number;
/** The currency of the product for the transaction */
currency?: string;
}
/**
* Build an Ecommerce Transaction Item Event
* Related to the {@link buildEcommerceTransaction}
* Each Ecommerce Transaction may contain one or more EcommerceTransactionItem events
*
* @param event - Contains the properties for the Ecommerce Transaction Item event
* @returns PayloadBuilder to be sent to {@link @snowplow/tracker-core#TrackerCore.track}
*/
export function buildEcommerceTransactionItem(event: EcommerceTransactionItemEvent): PayloadBuilder {
const { orderId, sku, price, name, category, quantity, currency } = event,
pb = payloadBuilder();
pb.add('e', 'ti'); // 'tr' for Transaction Item
pb.add('ti_id', orderId);
pb.add('ti_sk', sku);
pb.add('ti_nm', name);
pb.add('ti_ca', category);
pb.add('ti_pr', price);
pb.add('ti_qu', quantity);
pb.add('ti_cu', currency);
return pb;
}
/**
* A Screen View Event
* Similar to a Page View but less focused on typical web properties
* Often used for mobile applications as the user is presented with
* new views as they performance navigation events
*/
export interface ScreenViewEvent {
/** The name of the screen */
name?: string;
/** The identifier of the screen */
id?: string;
}
/**
* Build a Scren View Event
* Similar to a Page View but less focused on typical web properties
* Often used for mobile applications as the user is presented with
* new views as they performance navigation events
*
* @param event - Contains the properties for the Screen View event. One or more properties must be included.
* @returns PayloadBuilder to be sent to {@link @snowplow/tracker-core#TrackerCore.track}
*/
export function buildScreenView(event: ScreenViewEvent): PayloadBuilder {
const { name, id } = event;
return buildSelfDescribingEvent({
event: {
schema: 'iglu:com.snowplowanalytics.snowplow/screen_view/jsonschema/1-0-0',
data: removeEmptyProperties({ name, id }),
},
});
}
/**
* A Link Click Event
* Used when a user clicks on a link on a webpage, typically an anchor tag `<a>`
*/
export interface LinkClickEvent {
/** The target URL of the link */
targetUrl: string;
/** The ID of the element clicked if present */
elementId?: string;
/** An array of class names from the element clicked */
elementClasses?: Array<string>;
/** The target value of the element if present */
elementTarget?: string;
/** The content of the element if present and enabled */
elementContent?: string;
}
/**
* Build a Link Click Event
* Used when a user clicks on a link on a webpage, typically an anchor tag `<a>`
*
* @param event - Contains the properties for the Link Click event
* @returns PayloadBuilder to be sent to {@link @snowplow/tracker-core#TrackerCore.track}
*/
export function buildLinkClick(event: LinkClickEvent): PayloadBuilder {
const { targetUrl, elementId, elementClasses, elementTarget, elementContent } = event;
const eventJson = {
schema: 'iglu:com.snowplowanalytics.snowplow/link_click/jsonschema/1-0-1',
data: removeEmptyProperties({ targetUrl, elementId, elementClasses, elementTarget, elementContent }),
};
return buildSelfDescribingEvent({ event: eventJson });
}
/**
* An Ad Impression Event
* Used to track an advertisement impression
*
* @remarks
* If you provide the cost field, you must also provide one of 'cpa', 'cpc', and 'cpm' for the costModel field.
*/
export interface AdImpressionEvent {
/** Identifier for the particular impression instance */
impressionId?: string;
/** The cost model for the campaign */
costModel?: 'cpa' | 'cpc' | 'cpm';
/** Advertisement cost */
cost?: number;
/** The destination URL of the advertisement */
targetUrl?: string;
/** Identifier for the ad banner being displayed */
bannerId?: string;
/** Identifier for the zone where the ad banner is located */
zoneId?: string;
/** Identifier for the advertiser which the campaign belongs to */
advertiserId?: string;
/** Identifier for the advertiser which the campaign belongs to */
campaignId?: string;
}
/**
* Build a Ad Impression Event
* Used to track an advertisement impression
*
* @remarks
* If you provide the cost field, you must also provide one of 'cpa', 'cpc', and 'cpm' for the costModel field.
*
* @param event - Contains the properties for the Ad Impression event
* @returns PayloadBuilder to be sent to {@link @snowplow/tracker-core#TrackerCore.track}
*/
export function buildAdImpression(event: AdImpressionEvent): PayloadBuilder {
const { impressionId, costModel, cost, targetUrl, bannerId, zoneId, advertiserId, campaignId } = event;
const eventJson = {
schema: 'iglu:com.snowplowanalytics.snowplow/ad_impression/jsonschema/1-0-0',
data: removeEmptyProperties({
impressionId,
costModel,
cost,
targetUrl,
bannerId,
zoneId,
advertiserId,
campaignId,
}),
};
return buildSelfDescribingEvent({ event: eventJson });
}
/**
* An Ad Click Event
* Used to track an advertisement click
*
* @remarks
* If you provide the cost field, you must also provide one of 'cpa', 'cpc', and 'cpm' for the costModel field.
*/
export interface AdClickEvent {
/** The destination URL of the advertisement */
targetUrl: string;
/** Identifier for the particular click instance */
clickId?: string;
/** The cost model for the campaign */
costModel?: 'cpa' | 'cpc' | 'cpm';
/** Advertisement cost */
cost?: number;
/** Identifier for the ad banner being displayed */
bannerId?: string;
/** Identifier for the zone where the ad banner is located */
zoneId?: string;
/** Identifier for the particular impression instance */
impressionId?: string;
/** Identifier for the advertiser which the campaign belongs to */
advertiserId?: string;
/** Identifier for the advertiser which the campaign belongs to */
campaignId?: string;
}
/**
* Build a Ad Click Event
* Used to track an advertisement click
*
* @remarks
* If you provide the cost field, you must also provide one of 'cpa', 'cpc', and 'cpm' for the costModel field.
*
* @param event - Contains the properties for the Ad Click event
* @returns PayloadBuilder to be sent to {@link @snowplow/tracker-core#TrackerCore.track}
*/
export function buildAdClick(event: AdClickEvent): PayloadBuilder {
const { targetUrl, clickId, costModel, cost, bannerId, zoneId, impressionId, advertiserId, campaignId } = event;
const eventJson = {
schema: 'iglu:com.snowplowanalytics.snowplow/ad_click/jsonschema/1-0-0',
data: removeEmptyProperties({
targetUrl,
clickId,
costModel,
cost,
bannerId,
zoneId,
impressionId,
advertiserId,
campaignId,
}),
};
return buildSelfDescribingEvent({ event: eventJson });
}
/**
* An Ad Conversion Event
* Used to track an advertisement click
*
* @remarks
* If you provide the cost field, you must also provide one of 'cpa', 'cpc', and 'cpm' for the costModel field.
*/
export interface AdConversionEvent {
/** Identifier for the particular conversion instance */
conversionId?: string;
/** The cost model for the campaign */
costModel?: 'cpa' | 'cpc' | 'cpm';
/** Advertisement cost */
cost?: number;
/** Conversion category */
category?: string;
/** The type of user interaction e.g. 'purchase' */
action?: string;
/** Describes the object of the conversion */
property?: string;
/** How much the conversion is initially worth */
initialValue?: number;
/** Identifier for the advertiser which the campaign belongs to */
advertiserId?: string;
/** Identifier for the advertiser which the campaign belongs to */
campaignId?: string;
}
/**
* Build a Ad Conversion Event
* Used to track an advertisement click
*
* @remarks
* If you provide the cost field, you must also provide one of 'cpa', 'cpc', and 'cpm' for the costModel field.
*
* @param event - Contains the properties for the Ad Conversion event
* @returns PayloadBuilder to be sent to {@link @snowplow/tracker-core#TrackerCore.track}
*/
export function buildAdConversion(event: AdConversionEvent): PayloadBuilder {
const { conversionId, costModel, cost, category, action, property, initialValue, advertiserId, campaignId } = event;
const eventJson = {
schema: 'iglu:com.snowplowanalytics.snowplow/ad_conversion/jsonschema/1-0-0',
data: removeEmptyProperties({
conversionId,
costModel,
cost,
category,
action,
property,
initialValue,
advertiserId,
campaignId,
}),
};
return buildSelfDescribingEvent({ event: eventJson });
}
/**
* A Social Interaction Event
* Social tracking will be used to track the way users interact
* with Facebook, Twitter and Google + widgets
* e.g. to capture “like this” or “tweet this” events.
*/
export interface SocialInteractionEvent {
/** Social action performed */
action: string;
/** Social network */
network: string;
/** Object social action is performed on */
target?: string;
}
/**
* Build a Social Interaction Event
* Social tracking will be used to track the way users interact
* with Facebook, Twitter and Google + widgets
* e.g. to capture “like this” or “tweet this” events.
*
* @param event - Contains the properties for the Social Interaction event
* @returns PayloadBuilder to be sent to {@link @snowplow/tracker-core#TrackerCore.track}
*/
export function buildSocialInteraction(event: SocialInteractionEvent): PayloadBuilder {
const { action, network, target } = event;
const eventJson = {
schema: 'iglu:com.snowplowanalytics.snowplow/social_interaction/jsonschema/1-0-0',
data: removeEmptyProperties({ action, network, target }),
};
return buildSelfDescribingEvent({ event: eventJson });
}
/**
* An Add To Cart Event
* For tracking users adding items from a cart
* on an ecommerce site.
*/
export interface AddToCartEvent {
/** A Product Stock Keeping Unit (SKU) */
sku: string;
/** The number added to the cart */
quantity: number;
/** The name of the product */
name?: string;
/** The category of the product */
category?: string;
/** The price of the product */
unitPrice?: number;
/** The currency of the product */
currency?: string;
}
/**
* Build a Add To Cart Event
* For tracking users adding items from a cart
* on an ecommerce site.
*
* @param event - Contains the properties for the Add To Cart event
* @returns PayloadBuilder to be sent to {@link @snowplow/tracker-core#TrackerCore.track}
*/
export function buildAddToCart(event: AddToCartEvent): PayloadBuilder {
const { sku, quantity, name, category, unitPrice, currency } = event;
return buildSelfDescribingEvent({
event: {
schema: 'iglu:com.snowplowanalytics.snowplow/add_to_cart/jsonschema/1-0-0',
data: removeEmptyProperties({
sku,
quantity,
name,
category,
unitPrice,
currency,
}),
},
});
}
/**
* An Remove To Cart Event
* For tracking users removing items from a cart
* on an ecommerce site.
*/
export interface RemoveFromCartEvent {
/** A Product Stock Keeping Unit (SKU) */
sku: string;
/** The number removed from the cart */
quantity: number;
/** The name of the product */
name?: string;
/** The category of the product */
category?: string;
/** The price of the product */
unitPrice?: number;
/** The currency of the product */
currency?: string;
}
/**
* Build a Remove From Cart Event
* For tracking users removing items from a cart
* on an ecommerce site.
*
* @param event - Contains the properties for the Remove From Cart event
* @returns PayloadBuilder to be sent to {@link @snowplow/tracker-core#TrackerCore.track}
*/
export function buildRemoveFromCart(event: RemoveFromCartEvent): PayloadBuilder {
const { sku, quantity, name, category, unitPrice, currency } = event;
return buildSelfDescribingEvent({
event: {
schema: 'iglu:com.snowplowanalytics.snowplow/remove_from_cart/jsonschema/1-0-0',
data: removeEmptyProperties({
sku,
quantity,
name,
category,
unitPrice,
currency,
}),
},
});
}
/**
* Represents either a Form Focus or Form Change event
* When a user focuses on a form element or when a user makes a
* change to a form element.
*/
export interface FormFocusOrChangeEvent {
/** The schema which will be used for the event */
schema: 'change_form' | 'focus_form';
/** The ID of the form which the element belongs to */
formId: string;
/** The element ID which the user is interacting with */
elementId: string;
/** The name of the node ("INPUT", "TEXTAREA", "SELECT") */
nodeName: string;
/** The value of the element at the time of the event firing */
value: string | null;
/** The type of element (e.g. "datetime", "text", "radio", etc.) */
type?: string | null;
/** The class names on the element */
elementClasses?: Array<string> | null;
}
/**
* Build a Form Focus or Change Form Event based on schema property
* When a user focuses on a form element or when a user makes a
* change to a form element.
*
* @param event - Contains the properties for the Form Focus or Change Form event
* @returns PayloadBuilder to be sent to {@link @snowplow/tracker-core#TrackerCore.track}
*/
export function buildFormFocusOrChange(event: FormFocusOrChangeEvent): PayloadBuilder {
let event_schema = '';
const { schema, formId, elementId, nodeName, elementClasses, value, type } = event;
const event_data: Record<string, unknown> = { formId, elementId, nodeName, elementClasses, value };
if (schema === 'change_form') {
event_schema = 'iglu:com.snowplowanalytics.snowplow/change_form/jsonschema/1-0-0';
event_data.type = type;
} else if (schema === 'focus_form') {
event_schema = 'iglu:com.snowplowanalytics.snowplow/focus_form/jsonschema/1-0-0';
event_data.elementType = type;
}
return buildSelfDescribingEvent({
event: {
schema: event_schema,
data: removeEmptyProperties(event_data, { value: true }),
},
});
}
/**
* A representation of an element within a form
*/
export type FormElement = {
/** The name of the element */
name: string;
/** The current value of the element */
value: string | null;
/** The name of the node ("INPUT", "TEXTAREA", "SELECT") */
nodeName: string;
/** The type of element (e.g. "datetime", "text", "radio", etc.) */
type?: string | null;
};
/**
* A Form Submission Event
* Used to track when a user submits a form
*/
export interface FormSubmissionEvent {
/** The ID of the form */
formId: string;
/** The class names on the form */
formClasses?: Array<string>;
/** The elements contained within the form */
elements?: Array<FormElement>;
}
/**
* Build a Form Submission Event
* Used to track when a user submits a form
*
* @param event - Contains the properties for the Form Submission event
* @returns PayloadBuilder to be sent to {@link @snowplow/tracker-core#TrackerCore.track}
*/
export function buildFormSubmission(event: FormSubmissionEvent): PayloadBuilder {
const { formId, formClasses, elements } = event;
return buildSelfDescribingEvent({
event: {
schema: 'iglu:com.snowplowanalytics.snowplow/submit_form/jsonschema/1-0-0',
data: removeEmptyProperties({ formId, formClasses, elements }),
},
});
}
/**
* A Site Search Event
* Used when a user performs a search action on a page
*/
export interface SiteSearchEvent {
/** The terms of the search */
terms: Array<string>;
/** Any filters which have been applied to the search */
filters?: Record<string, string | boolean>;
/** The total number of results for this search */
totalResults?: number;
/** The number of visible results on the page */
pageResults?: number;
}
/**
* Build a Site Search Event
* Used when a user performs a search action on a page
*
* @param event - Contains the properties for the Site Search event
* @returns PayloadBuilder to be sent to {@link @snowplow/tracker-core#TrackerCore.track}
*/
export function buildSiteSearch(event: SiteSearchEvent): PayloadBuilder {
const { terms, filters, totalResults, pageResults } = event;
return buildSelfDescribingEvent({
event: {
schema: 'iglu:com.snowplowanalytics.snowplow/site_search/jsonschema/1-0-0',
data: removeEmptyProperties({ terms, filters, totalResults, pageResults }),
},
});
}
/**
* A Consent Withdrawn Event
* Used for tracking when a user withdraws their consent
*/
export interface ConsentWithdrawnEvent {
/** Specifies whether all consent should be withdrawn */
all: boolean;
/** Identifier for the document withdrawing consent */
id?: string;
/** Version of the document withdrawing consent */
version?: string;
/** Name of the document withdrawing consent */
name?: string;
/** Description of the document withdrawing consent */
description?: string;
}
/**
* Interface for returning a built event (PayloadBuilder) and context (Array of SelfDescribingJson).
*/
export interface EventPayloadAndContext {
/** Tracker payload for the event data */
event: PayloadBuilder;
/** List of context entities to track along with the event */
context: Array<SelfDescribingJson>;
}
/**
* Build a Consent Withdrawn Event
* Used for tracking when a user withdraws their consent
*
* @param event - Contains the properties for the Consent Withdrawn event
* @returns An object containing the PayloadBuilder to be sent to {@link @snowplow/tracker-core#TrackerCore.track} and a 'consent_document' context
*/
export function buildConsentWithdrawn(event: ConsentWithdrawnEvent): EventPayloadAndContext {
const { all, id, version, name, description } = event;
const documentJson = {
schema: 'iglu:com.snowplowanalytics.snowplow/consent_document/jsonschema/1-0-0',
data: removeEmptyProperties({ id, version, name, description }),
};
return {
event: buildSelfDescribingEvent({
event: {
schema: 'iglu:com.snowplowanalytics.snowplow/consent_withdrawn/jsonschema/1-0-0',
data: removeEmptyProperties({
all: all,
}),
},
}),
context: [documentJson],
};
}
/**
* A Consent Granted Event
* Used for tracking when a user grants their consent
*/
export interface ConsentGrantedEvent {
/** Identifier for the document granting consent */
id: string;
/** Version of the document granting consent */
version: string;
/** Name of the document granting consent */
name?: string;
/** Description of the document granting consent */
description?: string;
/** When the consent expires */
expiry?: string;
}
/**
* Build a Consent Granted Event
* Used for tracking when a user grants their consent
*
* @param event - Contains the properties for the Consent Granted event
* @returns An object containing the PayloadBuilder to be sent to {@link @snowplow/tracker-core#TrackerCore.track} and a 'consent_document' context
*/
export function buildConsentGranted(event: ConsentGrantedEvent): EventPayloadAndContext {
const { expiry, id, version, name, description } = event;
const documentJson = {
schema: 'iglu:com.snowplowanalytics.snowplow/consent_document/jsonschema/1-0-0',
data: removeEmptyProperties({ id, version, name, description }),
};
return {
event: buildSelfDescribingEvent({
event: {
schema: 'iglu:com.snowplowanalytics.snowplow/consent_granted/jsonschema/1-0-0',
data: removeEmptyProperties({
expiry: expiry,
}),
},
}),
context: [documentJson],
};
}
/**
* Returns a copy of a JSON with undefined and null properties removed
*
* @param event - JSON object to clean
* @param exemptFields - Set of fields which should not be removed even if empty
* @returns A cleaned copy of eventJson
*/
export function removeEmptyProperties(
event: Record<string, unknown>,
exemptFields: Record<string, boolean> = {}
): Record<string, unknown> {
const ret: Record<string, unknown> = {};
for (const k in event) {
if (exemptFields[k] || (event[k] !== null && typeof event[k] !== 'undefined')) {
ret[k] = event[k];
}
}
return ret;
}