salsify-experiences-sdk
Version:
SDK to be used by commerce websites to implement product experiences.
247 lines (218 loc) • 6.63 kB
text/typescript
import SdkOptions from './options'
import SdkSettings, { EnhancedContentSettings } from './settings'
import EnhancedContentApi, { attachIframeContextListener } from './enhancedContent'
import EventsApi from './events'
import { getCookie, setCookie, deleteCookie } from './utils/cookies'
import { createLogger } from './utils/logger'
import TimeOnPageTracker from './utils/time-on-page-tracker'
import { SDK_VERSION } from './version'
import { inBrowser } from './utils/runtime'
const sessionIdKey = 'salsify_session_id'
const defaultOptions = {
languageCode: 'en-US',
tracking: true,
}
const defaultEcSettings = {
idType: 'SDKID',
}
/** @internal */
export interface Context {
/** URL of the website serving the enhanced content, if operating within the browser */
url: string | undefined
/** UUID for the user visiting the website if operating within the browser,
* stored in / retrieved from a session cookie unless tracking is set to false
*
* If tracking is false, this is set to "NOT_TRACKED"
*/
sessionId: string | undefined
/** UUID for the page visit, if operating within the browser
*
* Not stored in a cookie; reset across page visits
*/
pageSessionId: string | undefined
/** Whether or not cookie-based user tracking is enabled. The host site is responsible
* for displaying any user consent dialogs and setting this option based on the user's
* preference.
*/
tracking: boolean
/** Client identifier (UUID) provided by Salsify that uniquely identifies the website */
clientId: string
/** Locale identifier that determines the language of the page content */
languageCode: string
/** Enhanced Content settings, including the product identifier type */
enhancedContent: EnhancedContentSettings
/** SDK version */
version: string
jsSource: 'bundle' | 'npm'
}
/**
* The Salsify Experiences SDK.
*
* This class is responsible for initializing the SDK and exposing the public getter methods for each different available API.
*/
export default class SdkApi {
#initialized = false
#settings?: SdkSettings
#ecApi?: EnhancedContentApi
#eventsApi?: EventsApi
#context?: Context
#stalePageSessionId = false
#timeOnPageTracker?: TimeOnPageTracker
#jsSource: 'bundle' | 'npm'
/** @internal */
public constructor(jsSource: 'bundle' | 'npm') {
this.#jsSource = jsSource
}
/**
* Initializes the SDK.
*
* @example
* ```javascript
* window.salsifyExperiencesSdk.init({ clientId, enhancedContent: { idType }})
* ```
*
* @param options The options to initialize the SDK with.
*/
public init(options: SdkOptions): void {
if (this.#initialized) {
throw new Error('Salsify Experiences SDK has already been initialized.')
}
const ecSettings = {
...defaultEcSettings,
...options.enhancedContent,
}
this.#settings = {
...defaultOptions,
...options,
enhancedContent: ecSettings,
}
this.#context = {
url: inBrowser() ? window.location.href : undefined,
sessionId: this.#updateSessionId(this.#settings.tracking),
pageSessionId: inBrowser() ? crypto.randomUUID?.() : undefined,
tracking: this.#settings.tracking,
clientId: this.#settings.clientId,
languageCode: this.#settings.languageCode,
enhancedContent: this.#settings.enhancedContent,
version: SDK_VERSION,
jsSource: this.#jsSource,
}
if (inBrowser()) {
attachIframeContextListener(this.#context)
}
const logger = createLogger(this.#context, this.#settings)
logger.log('init', this.#settings)
this.#ecApi = new EnhancedContentApi(this.#settings, this.#context, logger, {
beforeRender: this.#beforeRender.bind(this),
afterRender: this.#afterRender.bind(this),
})
if (inBrowser()) {
this.#timeOnPageTracker = new TimeOnPageTracker(logger, this.#ecApi)
this.#timeOnPageTracker.start()
}
this.#eventsApi = new EventsApi(logger, { beforeNavigation: this.#beforeNavigation.bind(this) }, this.#ecApi)
this.#initialized = true
}
/**
* Whether the SDK has been initialized.
*
* @example
* ```javascript
* const salsify = window.salsifyExperiencesSdk;
* salsify.initialized; // false
* salsify.init({ clientId, enhancedContent: { idType } });
* salsify.initialized; // true
* ```
*/
public get initialized(): boolean {
return this.#initialized
}
/**
* The initialized Enhanced Content API.
*
* Throws an error if the SDK has not been initialized.
*
* @example
* ```javascript
* const salsify = window.salsifyExperiencesSdk;
* const ec = salsify.enhancedContent;
* ```
*/
public get enhancedContent(): EnhancedContentApi {
if (!this.#ecApi) {
throw new Error('Salsify Experiences SDK has not been initialized.')
}
return this.#ecApi
}
/**
* The initialized Events API.
*
* Throws an error if the SDK has not been initialized.
*
* @example
* ```javascript
* const salsify = window.salsifyExperiencesSdk;
* const events = salsify.events;
* ```
*/
public get events(): EventsApi {
if (!this.#eventsApi) {
throw new Error('Salsify Experiences SDK has not been initialized.')
}
return this.#eventsApi
}
#getOrCreateSessionId(): string | undefined {
let id = getCookie(sessionIdKey)
if (!id) {
id = crypto.randomUUID?.()
if (id) {
setCookie(sessionIdKey, id)
}
}
return id
}
#removeSessionId(): void {
deleteCookie(sessionIdKey)
}
#resetPageSessionId(): void {
if (this.#context) {
this.#context.pageSessionId = crypto.randomUUID?.()
this.#stalePageSessionId = false
}
}
#beforeRender(): void {
if (!inBrowser()) return
if (this.#context) {
this.#context.url = window.location.href
}
if (this.#stalePageSessionId) {
this.#resetPageSessionId()
}
}
#afterRender(): void {
if (inBrowser()) {
this.#stalePageSessionId = true
}
}
#beforeNavigation(): void {
if (!inBrowser()) return
if (this.#context) {
this.#context.url = window.location.href
}
if (this.#stalePageSessionId) {
this.#timeOnPageTracker?.sendEvent()
this.#timeOnPageTracker?.restart()
this.#resetPageSessionId()
}
}
#updateSessionId(tracking: boolean): string | undefined {
if (inBrowser()) {
if (tracking) {
return this.#getOrCreateSessionId()
} else {
this.#removeSessionId()
return 'NOT_TRACKED'
}
}
}
}