diff --git a/packages/clarity-devtools/src/config.ts b/packages/clarity-devtools/src/config.ts index cd4711a9..5b4d484c 100644 --- a/packages/clarity-devtools/src/config.ts +++ b/packages/clarity-devtools/src/config.ts @@ -18,5 +18,8 @@ export default function(): Core.Config { ], fraud: true, checksum: [], + liveSignalsActionConfigs: new Map(), + liveSignalsActionType: null, + liveSignalsCustomAction: (): void => { }, }; } diff --git a/packages/clarity-devtools/src/content.ts b/packages/clarity-devtools/src/content.ts index 72c55815..6fa6ec67 100644 --- a/packages/clarity-devtools/src/content.ts +++ b/packages/clarity-devtools/src/content.ts @@ -66,7 +66,10 @@ function wireup(settings: any): string { unmask: "$__unmask__$", content: "$__showText__$", upload: (data: string): void => { window.postMessage({ action: "upload", payload: data }, "*"); }, - projectId: "devtools" + projectId: "devtools", + liveSignalsActionConfigs: new Map(), + liveSignalsActionType: 0, + liveSignalsCustomAction: () => {} }); }).toString(); Object.keys(settings).forEach(s => code = code.replace(`"$__${s}__$"`, JSON.stringify(settings[s]))); diff --git a/packages/clarity-js/src/clarity.ts b/packages/clarity-js/src/clarity.ts index b1b12528..a871d4bb 100644 --- a/packages/clarity-js/src/clarity.ts +++ b/packages/clarity-js/src/clarity.ts @@ -11,7 +11,7 @@ import * as interaction from "@src/interaction"; import * as layout from "@src/layout"; import * as performance from "@src/performance"; export { version }; -export { consent, event, identify, set, upgrade, metadata } from "@src/data"; +export { consent, event, identify, set, upgrade, metadata, signal } from "@src/data"; export { hashText } from "@src/layout"; const modules: Module[] = [diagnostic, layout, interaction, performance]; diff --git a/packages/clarity-js/src/core/action.ts b/packages/clarity-js/src/core/action.ts new file mode 100644 index 00000000..20257911 --- /dev/null +++ b/packages/clarity-js/src/core/action.ts @@ -0,0 +1,124 @@ +import config from "@src/core/config"; +import { signal } from "@src/data/signal"; +import { LiveSignalsActionType } from "@clarity-types/data"; + +export function determineAction(): void { + switch (config.liveSignalsActionType) { + case LiveSignalsActionType.NoAction: + // No action + signal(null); + break; + case LiveSignalsActionType.CustomAction: + // A custom action written by the client + signal(config.liveSignalsCustomAction); + break; + case LiveSignalsActionType.ContactUsDialogueBox: + signal(showDialog); + break; + case LiveSignalsActionType.ContactUsAlert: + signal(showAlert); + break; + default: + signal(null); + break; + } +} + +function showAlert(): void { + // Shows a blocking warning + var alertMessage: string = "It seems you might be facing some troubles. Please contact us at: "; + if ("email" in config.liveSignalsActionConfigs) + { + // The client email for example + alertMessage += config.liveSignalsActionConfigs["email"]; + } + alert(alertMessage); +} + +function showDialog(): void { + // Shows a non-blocking dialog box + // This function may be further optimized and cleaned up + var dialogMessage: string = "It seems you might be facing some troubles. Please contact us at: "; + if ("email" in config.liveSignalsActionConfigs) + { + // The client email for example + dialogMessage += config.liveSignalsActionConfigs["email"]; + } + + var lightmode : boolean = false; + if ("lightmode" in config.liveSignalsActionConfigs) + { + if (typeof(config.liveSignalsActionConfigs["lightmode"]) == "string") + { + lightmode = (config.liveSignalsActionConfigs["lightmode"].toLowerCase() == "true")? true : false; + } + } + + try { + // Tries to get the dialog element if it exists + const dialog = document.getElementById("zdialog") as HTMLDialogElement; // using a weird ID to avoid potential conflict with existing IDs + dialog.innerHTML = dialogMessage; + styleDialog(dialog, lightmode); + } catch (err) { + // If not, creates a new dialog element + const dialog = document.createElement("dialog") as HTMLDialogElement; + // dialog.innerHTML = "Non-Blocking test dialog 2"; + dialog.innerHTML = dialogMessage; + styleDialog(dialog, lightmode); + } +} + +function closeDialog(): void { + // No need to try catch, as this will only be called after + // showDialog, which means the dialog element exists + const dialog = document.getElementById("zdialog") as HTMLDialogElement; + dialog.style.display = "none"; +} + +function styleDialog(dialog: HTMLDialogElement, lightMode = false): void { + dialog.style.display = "block"; + dialog.id = "zdialog"; // Choosing a weird name to avoid any potential conflicts with other elements + + // Apply common styling + dialog.style.position = "fixed"; + dialog.style.top = "50%"; + dialog.style.left = "50%"; + dialog.style.transform = "translate(-50%, -50%)"; + dialog.style.margin = "0"; + dialog.style.padding = "20px"; + dialog.style.width = "80%"; + dialog.style.textAlign = "center"; + dialog.style.zIndex = "10000"; // Set the z-index to 10000 to make sure it is on top of everything + + // Apply mode-specific styling + if (lightMode) { + dialog.style.backgroundColor = "rgba(39, 54, 223, 0.93)"; + dialog.style.color = "rgb(245, 245, 245)"; + dialog.style.border = "2px solid #ddd"; + } else { + dialog.style.backgroundColor = "rgba(0, 0, 0, 0.9)"; + dialog.style.color = "#fff"; + dialog.style.border = "2px solid #666"; + } + + // Create a close button + const close = document.createElement("button"); + close.innerHTML = "OK"; + close.onclick = closeDialog; + dialog.appendChild(close); + + // Apply styling to the close button + close.style.backgroundColor = "#4CAF50"; + close.style.color = "#fff"; + close.style.padding = "10px 20px"; + close.style.border = "none"; + close.style.borderRadius = "4px"; + close.style.fontSize = "16px"; + close.style.cursor = "pointer"; + close.style.textShadow = "1px 1px #000"; + close.style.display = "block"; + close.style.margin = "auto"; + close.style.marginTop = "20px"; + + document.body.appendChild(dialog); // Append dialog to the body element +} \ No newline at end of file diff --git a/packages/clarity-js/src/core/config.ts b/packages/clarity-js/src/core/config.ts index 45b1dc9f..1528b142 100644 --- a/packages/clarity-js/src/core/config.ts +++ b/packages/clarity-js/src/core/config.ts @@ -18,7 +18,10 @@ let config: Config = { fallback: null, upgrade: null, action: null, - dob: null + dob: null, + liveSignalsActionConfigs: new Map(), + liveSignalsActionType: null, + liveSignalsCustomAction: null }; export default config; diff --git a/packages/clarity-js/src/core/index.ts b/packages/clarity-js/src/core/index.ts index 1bae028a..85aafabe 100644 --- a/packages/clarity-js/src/core/index.ts +++ b/packages/clarity-js/src/core/index.ts @@ -51,7 +51,17 @@ export function config(override: Config): boolean { // Process custom configuration overrides, if available if (override === null || status) { return false; } for (let key in override) { - if (key in configuration) { configuration[key] = override[key]; } + if (key in configuration) { + if (key === "liveSignalsCustomAction") { + // Check if there are some configs sent by the client + let liveSignalsConfigs: string = ('liveSignalsActionConfigs' in override)? "variables = override['liveSignalsActionConfigs'];" : ""; + // This way, the client can use their parameters in their custom action through variables["keyName"] + let liveSignalsCustomCode: string = String(override[key]).replace(/\n/g, "\\n");; + configuration[key] = eval("() => {" + liveSignalsConfigs + liveSignalsCustomCode + "}"); + continue; + } + configuration[key] = override[key]; + } } return true; } diff --git a/packages/clarity-js/src/core/signal.ts b/packages/clarity-js/src/core/signal.ts new file mode 100644 index 00000000..883130a1 --- /dev/null +++ b/packages/clarity-js/src/core/signal.ts @@ -0,0 +1,34 @@ +import { ClaritySignal } from "@clarity-types/data"; +import { signalCallback } from "@src/data/signal"; +import { determineAction } from "@src/core/action"; + +function parseSignals(signalsString: string): ClaritySignal[] { + const signalsJson: ClaritySignal[] = JSON.parse(signalsString) + return signalsJson; +} + + +const LiveSignalType: Map = new Map([ + ["BotSignal", 0], + ["FrustrationScore", 1], + ["PurchaseProbability", 2] + ]); + +export function signalEvent(signalsString: string) { + let botThreshold : number = 0.5; + let frustrationThreshold : number = 0.65; + let purchaseThreshold : number = 0.5; + const thresholds : number[] = [botThreshold, frustrationThreshold, purchaseThreshold]; + try { + const signals = parseSignals(signalsString); + + // Check whether user is frustrated or not + // if yes, execute an action based on configuration + if(!signalCallback) determineAction(); + signals.forEach(signal => { + if (signal["value"] > thresholds[LiveSignalType.get(signal["type"])]) { + signalCallback(signal); + } + }) + } catch (err) { } +} diff --git a/packages/clarity-js/src/data/index.ts b/packages/clarity-js/src/data/index.ts index 4010efd8..4c5fc040 100644 --- a/packages/clarity-js/src/data/index.ts +++ b/packages/clarity-js/src/data/index.ts @@ -16,6 +16,7 @@ export { event } from "@src/data/custom"; export { consent, metadata } from "@src/data/metadata"; export { upgrade } from "@src/data/upgrade"; export { set, identify } from "@src/data/variable"; +export { signal } from "@src/data/signal"; const modules: Module[] = [baseline, dimension, variable, limit, summary, metadata, envelope, upload, ping, upgrade, extract]; diff --git a/packages/clarity-js/src/data/signal.ts b/packages/clarity-js/src/data/signal.ts new file mode 100644 index 00000000..c5eb647a --- /dev/null +++ b/packages/clarity-js/src/data/signal.ts @@ -0,0 +1,7 @@ +import { SignalCallback } from "@clarity-types/data"; + +export let signalCallback: SignalCallback = null; + +export function signal(cb: SignalCallback): void { + signalCallback = cb +} \ No newline at end of file diff --git a/packages/clarity-js/src/data/upload.ts b/packages/clarity-js/src/data/upload.ts index cd9c7191..eeab6033 100644 --- a/packages/clarity-js/src/data/upload.ts +++ b/packages/clarity-js/src/data/upload.ts @@ -3,6 +3,7 @@ import { BooleanFlag, Check, Constant, EncodedPayload, Event, Metric, Setting, T import * as clarity from "@src/clarity"; import config from "@src/core/config"; import measure from "@src/core/measure"; +import { signalEvent } from "@src/core/signal"; import { time } from "@src/core/time"; import { clearTimeout, setTimeout } from "@src/core/timeout"; import compress from "@src/data/compress"; @@ -267,6 +268,9 @@ function response(payload: string): void { case Constant.Extract: if (parts.length > 1) { extract.trigger(parts[1]); } break; + case Constant.Signal: + if (parts.length > 1) { signalEvent(parts[1]) } + break; } } } diff --git a/packages/clarity-js/types/core.d.ts b/packages/clarity-js/types/core.d.ts index 7bf34406..39eee13b 100644 --- a/packages/clarity-js/types/core.d.ts +++ b/packages/clarity-js/types/core.d.ts @@ -137,6 +137,9 @@ export interface Config { upgrade?: (key: string) => void; action?: (key: string) => void; dob?: number; + liveSignalsActionConfigs?: Map; + liveSignalsActionType?: number; + liveSignalsCustomAction?: Data.SignalCallback; } export const enum Constant { diff --git a/packages/clarity-js/types/data.d.ts b/packages/clarity-js/types/data.d.ts index c1b93348..8b29867e 100644 --- a/packages/clarity-js/types/data.d.ts +++ b/packages/clarity-js/types/data.d.ts @@ -8,6 +8,7 @@ export interface MetadataCallbackOptions { callback: MetadataCallback, wait: boolean } +export type SignalCallback = (data: ClaritySignal) => void; /* Enum */ export const enum Event { @@ -273,6 +274,7 @@ export const enum Constant { End = "END", Upgrade = "UPGRADE", Action = "ACTION", + Signal = "SIGNAL", Extract = "EXTRACT", UserHint = "userHint", UserType = "userType", @@ -313,6 +315,15 @@ export const enum XMLReadyState { Done = 4 } +/* Live Signals */ + +export const enum LiveSignalsActionType { + NoAction = 0, + CustomAction = 1, + ContactUsDialogueBox = 2, + ContactUsAlert = 3 +} + /* Helper Interfaces */ export interface Payload { @@ -439,3 +450,9 @@ export interface UploadData { attempts: number; status: number; } + +export interface ClaritySignal { + type: string + timestamp?: number + value?: number +} \ No newline at end of file diff --git a/packages/clarity-js/types/index.d.ts b/packages/clarity-js/types/index.d.ts index 8c3d66a5..79c1dfe1 100644 --- a/packages/clarity-js/types/index.d.ts +++ b/packages/clarity-js/types/index.d.ts @@ -16,6 +16,7 @@ interface Clarity { set: (variable: string, value: string | string[]) => void; identify: (userId: string, sessionId?: string, pageId?: string, userHint?: string) => void; metadata: (callback: Data.MetadataCallback, wait?: boolean) => void; + signal: (callback: Data.SignalCallback) => void; } interface Selector {