import { BoundingBox } from "./FeatureTemplate";
function getCurrentUser() {
const clientAgent: Record<string, string> = {};
clientAgent["uuid"] = localStorage["client_agent_uuid"];
return clientAgent;
}
function getMetadata() {
const clientAgent: Record<string, string> = {};
const startIndex = navigator.userAgent.search("\\(");
const endIndex = navigator.userAgent.search("\\)");
clientAgent["device"] = navigator.userAgent.substring(
startIndex + 1,
endIndex,
);
clientAgent["platform"] = navigator.userAgent.substring(endIndex + 2);
return clientAgent;
}
function getClientAgent() {
return "client_agent_uuid" in localStorage
? getCurrentUser()
: getMetadata();
}
function setCurrentUser(uuid: string) {
localStorage["client_agent_uuid"] = uuid;
}
type LoadData = {
type: "ENTER";
timestamp: number;
payload: {
country: string;
client_agent: ReturnType<typeof getClientAgent>;
timezone_offset_mins: number;
};
};
type UnLoadData = {
type: "CLEAN_EXIT";
timestamp: number;
payload: {
session_uuid: string;
};
};
export type EffectDataPayload = {
session_uuid: string;
shade_color?: number;
session_product_interaction?: number;
// --- from effect
application_area_name: string;
rendering_hex: string;
finish_name: string;
color_opacity: number;
finish_opacity: number;
bounding_box: BoundingBox;
effect: number;
};
export type EffectData = {
type: "EFFECT_SIMULATED" | "EFFECT_DISABLED";
timestamp: number;
payload: EffectDataPayload;
};
export type ProductData = {
type: "PRODUCT_SIMULATED" | "PRODUCT_DISABLED";
timestamp: number;
payload: {
session_uuid: string;
product: number;
};
};
async function sendAnalyticsData(
data: EffectData | ProductData | LoadData | UnLoadData,
cmsURL: string,
accessToken: string,
) {
try {
const url = `${cmsURL}/analytics?access_token=${accessToken}`;
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
keepalive: true,
});
if (response.status === 404) {
throw new Error("Resource not found (404)");
} else if (!response.ok) {
throw new Error(`An error occurred: ${response.status}`);
}
return await response.json();
} catch (error) {
return null;
}
}
export class SdkAnalytics {
cmsURL: string;
accessToken: string;
sessionUuid: string;
private constructor(
cmsURL: string,
accessToken: string,
sessionUuid: string,
) {
this.cmsURL = cmsURL;
this.accessToken = accessToken;
this.sessionUuid = sessionUuid;
}
/**
* This is part of "async constructor" function create.
* It is to request sessionId when analytics object is initialized.
* Extracted only so that function is more managable/abstract. Keep them together.
*/
private static async requestSessionId(
cmsURL: string,
accessToken: string,
): Promise<string> {
const clientAgent = getClientAgent();
const data: LoadData = {
type: "ENTER",
timestamp: new Date().getTime(),
payload: {
country: Intl.DateTimeFormat().resolvedOptions().timeZone, // TODO (in BEA-4635?),
client_agent: clientAgent,
timezone_offset_mins: new Date().getTimezoneOffset(),
},
};
const enterResult = await sendAnalyticsData(data, cmsURL, accessToken);
const sessionUuid = enterResult?.data?.[0]?.session_uuid;
if (!("client_agent_uuid" in localStorage)) {
setCurrentUser(enterResult.data[0].client_agent_uuid);
}
return sessionUuid;
}
static async create(
cmsURL: string,
accessToken: string,
): Promise<SdkAnalytics> {
if (!cmsURL) throw new Error("CMS URL is required");
if (!accessToken) throw new Error("Access token is required");
const session_uuid = await this.requestSessionId(cmsURL, accessToken);
return new SdkAnalytics(cmsURL, accessToken, session_uuid);
}
async onUnload() {
const data: UnLoadData = {
type: "CLEAN_EXIT",
timestamp: new Date().getTime(),
payload: {
session_uuid: this.sessionUuid,
},
};
await sendAnalyticsData(data, this.cmsURL, this.accessToken);
}
async sendEffect(
type: EffectData["type"],
data: Omit<EffectData["payload"], "session_uuid">,
) {
sendAnalyticsData(
{
type,
timestamp: new Date().getTime(),
payload: {
...data,
session_uuid: this.sessionUuid,
},
},
this.cmsURL,
this.accessToken,
);
}
/**
* @returns interaction_id of the created product interaction, (PS. if it did ?!?! *confusion*)
*/
async sendProduct(
type: ProductData["type"],
productId: number,
): Promise<number | undefined> {
const productSimulatedResult = await sendAnalyticsData(
{
type,
timestamp: new Date().getTime(),
payload: {
product: productId,
session_uuid: this.sessionUuid,
},
},
this.cmsURL,
this.accessToken,
);
const sessionProductInteractionId =
productSimulatedResult?.data[0]?.interaction_id ?? undefined;
// For some reason unbeknownst to me, it is expected to fail (undefined is expected result) and continue execution as everything is normal.
// If it was expected to behave that way there would be no need for this warning, and if that didn't fit somewhere else, it should be handled there, it's own semantics clear.
// Alas, leaving the warning here...
if (!sessionProductInteractionId) {
console.warn(
"sessionProductInteractionId returned by post request is undefined",
);
}
return sessionProductInteractionId;
}
/**
* UNSAFE: effectsWithoutInteraction is mutated.
*
* Convenience function for operations that go in series.
*
* https://visagetechnologies.atlassian.net/jira/software/c/projects/BEA/boards/38?selectedIssue=BEA-6931
* It doesn't solve second AC point
*
* @param productData product id for interaction payload data. If it's undefined, it will be skipped.
* @param effectsWithoutInteraction You can pass anything in `payload.session_product_interaction`, the value will be assigned internally if productData is passed in.
*/
async sendProductAndEffects(
productData: {
type: ProductData["type"];
productId: number;
} | null,
effectsDataWithoutInteraction: {
type: EffectData["type"];
payload: Omit<EffectData["payload"], "session_uuid">;
}[],
) {
if (productData) {
const interactionId = await this.sendProduct(
productData.type,
productData.productId,
);
for (const eff of effectsDataWithoutInteraction) {
eff.payload.session_product_interaction = interactionId;
}
}
const effectSends = effectsDataWithoutInteraction.map((eff) =>
this.sendEffect(eff.type, eff.payload),
);
await Promise.all(effectSends);
}
}