@zywave/zywave-api-toolkit-bundle
Version:
517 lines (507 loc) • 18.5 kB
JavaScript
import { c as css, p as property, Z as ZywaveBaseElement, h as html } from './_api-proxy-f3bf2df5.js';
const proxiedMethods = new Set();
function configureHistory() {
const emitHistoryEvent = function (type, eventName) {
if (proxiedMethods.has(type)) {
return window.history[type];
}
const orig = window.history[type];
proxiedMethods.add(type);
return function () {
const rv = orig.apply(this, arguments);
const e = new CustomEvent(eventName ?? type, {
detail: {
args: arguments
}
});
window.dispatchEvent(e);
return rv;
};
};
history.pushState = emitHistoryEvent("pushState", "zapiPushState");
history.replaceState = emitHistoryEvent("replaceState", "zapiReplaceState");
}
const style = css`:host{position:absolute;display:none;width:0;height:0;border:0}`;
function loadScript(scriptSrc, opts) {
return new Promise((resolve, reject) => {
let scriptSrcUrl;
if (typeof scriptSrc === "string") {
scriptSrcUrl = new URL(scriptSrc);
} else {
scriptSrcUrl = scriptSrc;
}
if (opts?.preconfigureUrl) {
scriptSrcUrl = opts.preconfigureUrl(scriptSrcUrl);
}
if (!document.querySelector(`script[src="${scriptSrcUrl}"]`)) {
const script = document.createElement("script");
script.type = "text/javascript";
script.async = true;
script.defer = true;
script.src = scriptSrcUrl.toString();
if (opts?.preconfigureScriptElement) {
opts.preconfigureScriptElement(script);
}
document.head.appendChild(script);
script.addEventListener("load", () => resolve());
script.addEventListener("error", () => reject());
} else {
resolve();
}
});
}
function getNetworkInfo() {
if (!window.navigator.connection) {
return undefined;
}
const networkConnectionType = window.navigator.connection.type === "unknown" ? undefined : window.navigator.connection.type;
return {
networkReducedData: window.navigator.connection.saveData ?? false,
networkConnectionType,
networkRtt: window.navigator.connection.rtt,
networkDownlink: window.navigator.connection.downlink,
networkDownlinkMax: window.navigator.connection.downlinkMax
};
}
function getUserPreferences() {
const prefersDarkMode = window.matchMedia?.("(prefers-color-scheme: dark)").matches ?? false;
return {
prefersDarkMode
};
}
function addComposedEventListener(element, type, listener) {
if (!Event.prototype.composedPath) {
return;
}
element.addEventListener(type, e => {
const targets = e.composedPath();
if (!targets || !targets.length) {
return;
}
if (targets[0] === e.target) {
return;
}
if (!(targets[0] instanceof Element)) {
return;
}
listener(e, targets[0]);
});
}
function getResolution() {
return `${window.screen.width}x${window.screen.height}`;
}
async function configureHeap(appId, identity, userProperties, eventProperties, cdnHost) {
eventProperties ??= {};
const scriptSrc = new URL("cdn.heapanalytics.com/js/", cdnHost);
function parseProperties(_, target) {
return {
"Target ID": target.id,
"Target Class": target.className,
"Target Tag": target.tagName,
"Target Text": target.textContent,
"Target Name": target.getAttribute("name"),
"Target Type": target.getAttribute("type"),
"Target Href": target.getAttribute("href"),
"Target Action": target.getAttribute("action")
};
}
function trackPage() {
window.heap.track("View");
}
try {
await loadScript(scriptSrc, {
preconfigureUrl: url => {
const heap = window.heap || [];
window.heap = heap;
heap.appid = appId;
heap.config = {};
if (identity) {
heap.push(["identify", identity]);
}
const isImpersonating = eventProperties?.isImpersonating ?? false;
if (userProperties) {
if ("userPrincipalId" in userProperties) {
userProperties["User Principal ID"] = userProperties.userPrincipalId;
delete userProperties.userPrincipalId;
}
if (!isImpersonating) {
const userPreferences = getUserPreferences();
// hack to get TS happy
userProperties = {
...userPreferences,
...userProperties
};
}
heap.push(["addUserProperties", userProperties]);
}
const networkInfo = getNetworkInfo();
const finalEventProps = {
screenWidth: window.screen.width,
screenHeight: window.screen.height,
screenResolution: getResolution(),
...networkInfo,
...eventProperties
};
if ("userPrincipalId" in finalEventProps) {
finalEventProps["User Principal ID"] = finalEventProps.userPrincipalId;
delete finalEventProps.userPrincipalId;
}
if ("isImpersonating" in finalEventProps) {
finalEventProps["User Profile Impersonated"] = (finalEventProps.isImpersonating ?? false).toString();
delete finalEventProps.isImpersonating;
}
heap.clearEventProperties?.();
heap.push(["addEventProperties", finalEventProps]);
heap.track = (name, ...args) => {
args.unshift(name, "track");
heap.push(args);
};
url = new URL(`heap-${appId}.js`, url);
return url;
}
});
if (window.heap) {
addComposedEventListener(window.document, "click", (e, target) => {
window.heap.track(`Composed click`, parseProperties(e, target));
});
addComposedEventListener(window.document, "submit", (e, target) => {
window.heap.track(`Composed submit`, parseProperties(e, target));
});
addComposedEventListener(window.document, "change", (e, target) => {
window.heap.track(`Composed change`, parseProperties(e, target));
});
window.addEventListener("zapiReplaceState", trackPage);
window.addEventListener("zapiPushState", trackPage);
}
return {
track(eventName, payload) {
window.heap?.track?.(eventName, payload);
},
trackerId: "Heap"
};
} catch {
return undefined;
}
}
async function configureAppcues(accountId, identity, userProperties, eventProperties, cdnHost) {
function trackPage() {
window.Appcues.page();
}
const scriptSrc = new URL("fast.appcues.com/", cdnHost);
try {
await loadScript(scriptSrc, {
preconfigureUrl: url => {
// AppCues has some AMD loader support; we're not using requirejs but standard script loading
// https://docs.appcues.com/article/303-using-appcues-with-requirejs
window.AppcuesSettings = Object.assign({}, window.AppcuesSettings, {
skipAMD: true
});
return new URL(`${accountId}.js`, url);
}
});
if (window.Appcues) {
import('./_appcues-css-7ed1f33f.js').then(exports => {
if (exports.style.styleSheet) {
document.adoptedStyleSheets = [...document.adoptedStyleSheets, exports.style.styleSheet];
}
});
// do not call identify when a profile is being impersonated
// Appcues identify appears to be a one-and-done call that does not update fields
if (identity && !eventProperties?.isImpersonating) {
let props;
if (userProperties) {
props = {
...userProperties,
first_name: userProperties.givenName,
last_name: userProperties.familyName
};
delete props.givenName;
delete props.familyName;
}
window.Appcues.identify(identity, props);
}
window.addEventListener("zapiReplaceState", trackPage);
window.addEventListener("zapiPushState", trackPage);
}
return {
track(_eventName, _payload) {},
trackerId: "Appcues"
};
} catch {
return undefined;
}
}
class Tracker {
static get instance() {
return this.#instance ??= new this();
}
static #instance;
#trackers = [];
constructor() {}
track(eventName, payload) {
for (const tracker of this.#trackers) {
tracker.then(t => t?.track(eventName, payload));
}
}
registerTracker(tracker) {
if (tracker) {
this.#trackers.push(Promise.resolve(tracker));
}
}
}
const AnalyticsTracker = Tracker.instance;
var __decorate = undefined && undefined.__decorate || function (decorators, target, key, desc) {
var c = arguments.length,
r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc,
d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
let DEFAULT_CDN_HOST = new URL(import.meta.url).origin;
if (DEFAULT_CDN_HOST.includes("unpkg.com")) {
DEFAULT_CDN_HOST = "https://cdn.zywave.com";
// eslint-disable-next-line no-console
console.warn("Loading the Zywave API Toolkit from unpkg.com is deprecated. Please use cdn.zywave.com instead. Your application is likely to break.");
}
function getLanguages() {
return navigator.languages.map(l => `[${l.toLowerCase()}]`).join(";");
}
/**
* `ZywaveAnalyticsElement` defines a configurable way to communicate with our centralized analytics tracking. Note: it's highly preferred to use `ZywaveShellElement` to do this for you; this should only be used if you cannot use `ZywaveShellElement`.
* @element zywave-analytics
*
* @event load - Fired when analytics scripts have finished loading
*
* @attr {string | null} [api-base-url=null] - Provide the base URL to the Zywave APIs e.g., https://api.zywave.com/ (Note: the trailing slash is critical, especially if the base URL includes a path.)
* @attr {string | null} [bearer-token=null] - (optional) Provide a Zywave bearer token for authorization
* @attr {string | null} [profile-token=null] - (optional) Provide the explicit profile token that your application understands this user to be accessing
*
* @prop {string | null} [apiBaseUrl=null] - Provide the base URL to the Zywave APIs e.g., https://api.zywave.com/ (Note: the trailing slash is critical, especially if the base URL includes a path.)
* @prop {string | null} [bearerToken=null] - (optional) Provide a Zywave bearer token for authorization
* @prop {string | null} [profileToken=null] - (optional) Provide the explicit profile token that your application understands this user to be accessing
*/
class ZywaveAnalyticsElement extends ZywaveBaseElement {
constructor() {
super(...arguments);
/**
* The app id to use when communicating with Heap.
* If not specified, the `/shell/v2.0/analyticsinfo` API will be used.
*/
this.heapAppId = null;
/**
* The account id to use when communicating with Appcues.
* If not specified, the `/shell/v2.0/analyticsinfo` API will be used.
*/
this.appcuesAccountId = null;
/**
* A uniquely identifying string for the authenticated user. For most apps, this should be in the form `${profileTypeCode}~${profileId}`.
* If not specified, the `/shell/v2.0/analyticsinfo` API will be used.
*/
this.identity = null;
/**
* An optional property to directly add `userProperties` to analytics utilities.
* `givenName`, `familyName`, and `email` are common properties to be used across all platforms; you can provide more properties to this object where applicable.
*/
this.userProperties = null;
/**
* If specified, will prevent scripts from loading and analytics being configured until after the document has been parsed
*/
this.defer = false;
/**
* If provided, will set the CDN host for all external script loading.
* @default new URL(import.meta.url).origin
*/
this.cdnHost = DEFAULT_CDN_HOST;
/**
* UNSTABLE: DO NOT USE
* @ignore
*/
this.contextPath = null;
this.#analyticsDataLoaded = false;
}
static get styles() {
return [style];
}
#activityTrackingIntervalId;
get #isConfigBased() {
return !!(this.heapAppId || this.appcuesAccountId || this.identity);
}
// this property is not a JS private field to enable testing
get _userProperties() {
const analyticsUserProperties = this.#analyticsData?.userProperties;
const userProperties = typeof this.userProperties === "object" ? this.userProperties : undefined;
const result = {
languages: getLanguages(),
...analyticsUserProperties,
...userProperties
};
// API is source of truth
if (this.#userData) {
result.email = this.#userData.email || result.email;
result.givenName = this.#userData.given_name || result.givenName;
result.familyName = this.#userData.family_name || result.familyName;
}
return result;
}
#analyticsData;
#userData;
#analyticsDataLoaded;
connectedCallback() {
super.connectedCallback();
configureHistory();
}
disconnectedCallback() {
super.disconnectedCallback();
this.#activityTrackingIntervalId && clearInterval(this.#activityTrackingIntervalId);
this.#activityTrackingIntervalId = undefined;
}
async firstUpdated(changedProps) {
super.firstUpdated(changedProps);
if (!this.#isConfigBased) {
await this._apiClientReady;
}
await Promise.allSettled([this.#loadAnalyticsInfo(), this.#loadUserInfo()]);
if (this.#analyticsDataLoaded) {
if (this.defer && document.readyState !== "complete") {
document.addEventListener("readystatechange", event => {
if (event.target.readyState === "complete") {
this.#configureAnalytics();
}
});
} else {
this.#configureAnalytics();
}
}
}
render() {
return html``;
}
#configureAnalytics() {
this.#configureActivityTracker();
const {
heapAppId,
appcuesAccountId,
identity,
eventProperties
} = this.#analyticsData ?? {};
const cdnHost = this.cdnHost ?? "https://cdn.zywave.com";
let heapPromise;
let appcuesPromise;
if (heapAppId) {
heapPromise = configureHeap(heapAppId, identity, this._userProperties, eventProperties, cdnHost);
}
if (appcuesAccountId) {
appcuesPromise = configureAppcues(appcuesAccountId, identity, this._userProperties, eventProperties, cdnHost);
}
AnalyticsTracker.registerTracker(heapPromise);
AnalyticsTracker.registerTracker(appcuesPromise);
Promise.allSettled([heapPromise, appcuesPromise]).then(() => {
this.dispatchEvent(new CustomEvent("load"));
});
}
/**
* Method used to track custom events.
* @param eventName Name of the event to track
* @param payload Optional payload to pass to the event
*/
track(eventName, payload) {
AnalyticsTracker.track(eventName, payload);
}
async #loadAnalyticsInfo() {
if (this.#isConfigBased) {
this.#analyticsData = {
heapAppId: this.heapAppId || "",
appcuesAccountId: this.appcuesAccountId || "",
identity: this.identity || "",
contextPath: this.contextPath || ""
};
this.#analyticsDataLoaded = true;
} else if (this._authorized && this._apiClient) {
const url = new URL("shell/v2.0/analyticsinfo", this.apiUrl);
url.searchParams.set("host", window.location.hostname);
const resp = await this._apiClient.get(url);
if (resp instanceof Response && resp.ok) {
const data = await resp.json();
this.#analyticsDataLoaded = true;
this.#analyticsData = data;
this.#analyticsData.contextPath ??= this.contextPath;
}
} else {
this.#analyticsDataLoaded = true;
}
}
async #loadUserInfo() {
if (this._authorized && this._apiClient) {
const url = new URL("userinfo", this.apiUrl);
const resp = await this._apiClient.fetch(url);
if (resp instanceof Response && resp.ok) {
const data = await resp.json();
this.#userData = data;
}
}
}
async #configureActivityTracker() {
if (!this.#analyticsData?.contextPath) {
return;
}
const {
ActivityTracker
} = await import('./_activity-tracker-3f601b32.js');
await ActivityTracker.connect({
isImpersonated: this.#analyticsData.isImpersonating ?? false,
cookieDomain: this.#analyticsData.cookieDomain
});
this.#activityTrackingIntervalId = window.setInterval(async () => {
await this.#recordActivities(ActivityTracker);
}, /* 5 seconds */this.#analyticsData?.activityTrackingInterval ?? 2_000);
window.addEventListener("visibilitychange", async () => {
if (document.visibilityState === "hidden") {
await this.#recordActivities(ActivityTracker);
}
});
}
async #recordActivities(tracker) {
const activity = await tracker.retrieveActivity();
if (!(this.#analyticsData?.contextPath && activity.length)) {
return;
}
// /a/e = /analytics/event, to cicumvent naive ad blockers
const url = new URL(`v3/shell/v1.0${this.#analyticsData.contextPath}/a/e`, this.apiUrl);
this._apiClient?.fetch(url, {
method: "POST",
body: JSON.stringify(activity),
headers: {
"Content-Type": "application/json"
},
keepalive: true
});
}
}
__decorate([property({
type: String,
attribute: "heap-app-id"
})], ZywaveAnalyticsElement.prototype, "heapAppId", void 0);
__decorate([property({
type: String,
attribute: "appcues-account-id"
})], ZywaveAnalyticsElement.prototype, "appcuesAccountId", void 0);
__decorate([property({
type: String
})], ZywaveAnalyticsElement.prototype, "identity", void 0);
__decorate([property({
type: Object,
attribute: "user-properties"
})], ZywaveAnalyticsElement.prototype, "userProperties", void 0);
__decorate([property({
type: Boolean
})], ZywaveAnalyticsElement.prototype, "defer", void 0);
__decorate([property({
type: String,
attribute: "cdn-host"
})], ZywaveAnalyticsElement.prototype, "cdnHost", void 0);
__decorate([property({
type: String,
attribute: "context-path"
})], ZywaveAnalyticsElement.prototype, "contextPath", void 0);
window.customElements.define("zywave-analytics", ZywaveAnalyticsElement);
export { AnalyticsTracker as A };