clarity-js
Version:
An analytics library that uses web page interactions to generate aggregated insights
145 lines (140 loc) • 6.82 kB
text/typescript
import { Privacy, Task, Timer } from "@clarity-types/core";
import { Event, Setting, Token } from "@clarity-types/data";
import { Constant, NodeInfo, NodeValue } from "@clarity-types/layout";
import config from "@src/core/config";
import * as scrub from "@src/core/scrub";
import * as task from "@src/core/task";
import { time } from "@src/core/time";
import tokenize from "@src/data/token";
import * as baseline from "@src/data/baseline";
import { queue } from "@src/data/upload";
import * as fraud from "@src/diagnostic/fraud";
import * as doc from "@src/layout/document";
import * as dom from "@src/layout/dom";
import * as region from "@src/layout/region";
import * as style from "@src/layout/style";
import * as animation from "@src/layout/animation";
import * as custom from "@src/layout/custom";
export default async function (type: Event, timer: Timer = null, ts: number = null): Promise<void> {
let eventTime = ts || time()
let tokens: Token[] = [eventTime, type];
switch (type) {
case Event.Document:
let d = doc.data;
tokens.push(d.width, d.height);
baseline.track(type, d.width, d.height);
queue(tokens);
break;
case Event.Region:
for (let r of region.state) {
tokens = [r.time, Event.Region];
tokens.push(r.data.id, r.data.interaction, r.data.visibility, r.data.name);
queue(tokens, false);
}
region.reset();
break;
case Event.StyleSheetAdoption:
case Event.StyleSheetUpdate:
for (let entry of style.sheetAdoptionState) {
tokens = [entry.time, entry.event];
tokens.push(entry.data.id, entry.data.operation, entry.data.newIds);
queue(tokens);
}
for (let entry of style.sheetUpdateState) {
tokens = [entry.time, entry.event];
tokens.push(entry.data.id, entry.data.operation, entry.data.cssRules);
queue(tokens, false);
}
style.reset();
break;
case Event.Animation:
for (let entry of animation.state) {
tokens = [entry.time, entry.event];
tokens.push(
entry.data.id, entry.data.operation,
entry.data.keyFrames, entry.data.timing,
entry.data.timeline, entry.data.targetId
);
queue(tokens);
}
animation.reset();
break;
case Event.Discover:
case Event.Mutation:
// Check if we are operating within the context of the current page
if (task.state(timer) === Task.Stop) { break; }
let values = dom.updates();
// Only encode and queue DOM updates if we have valid updates to report back
if (values.length > 0) {
for (let value of values) {
let state = task.state(timer);
if (state === Task.Wait) { state = await task.suspend(timer); }
if (state === Task.Stop) { break; }
let data: NodeInfo = value.data;
let active = value.metadata.active;
let suspend = value.metadata.suspend;
let privacy = value.metadata.privacy;
let mangle = shouldMangle(value);
let keys = active ? ["tag", "attributes", "value"] : ["tag"];
for (let key of keys) {
// we check for data[key] === '' because we want to encode empty strings as well, especially for value - which if skipped can cause our decoder to assume the final
// attribute was the value for the node
if (data[key] || data[key] === '') {
switch (key) {
case "tag":
let box = size(value);
let factor = mangle ? -1 : 1;
tokens.push(value.id * factor);
if (value.parent && active) {
tokens.push(value.parent);
if (value.previous) { tokens.push(value.previous); }
}
tokens.push(suspend ? Constant.SuspendMutationTag : data[key]);
if (box && box.length === 2) { tokens.push(Constant.Hash + box[0].toString(36) + "." + box[1].toString(36)); }
break;
case "attributes":
for (let attr in data[key]) {
if (data[key][attr] !== undefined) {
tokens.push(attribute(attr, data[key][attr], privacy, data.tag));
}
}
break;
case "value":
fraud.check(value.metadata.fraud, value.id, data[key]);
tokens.push(scrub.text(data[key], data.tag, privacy, mangle));
break;
}
}
}
}
if (type === Event.Mutation) { baseline.activity(eventTime); }
queue(tokenize(tokens), !config.lean);
}
break;
case Event.CustomElement:
for (let element of custom.elements) {
queue([eventTime, Event.CustomElement, element]);
}
custom.reset();
break;
}
}
function shouldMangle(value: NodeValue): boolean {
let privacy = value.metadata.privacy;
return value.data.tag === Constant.TextTag && !(privacy === Privacy.None || privacy === Privacy.Sensitive);
}
function size(value: NodeValue): number[] {
if (value.metadata.size !== null && value.metadata.size.length === 0) {
let img = dom.getNode(value.id) as HTMLImageElement;
if (img) {
return [Math.floor(img.offsetWidth * Setting.BoxPrecision), Math.floor(img.offsetHeight * Setting.BoxPrecision)];
}
}
return value.metadata.size;
}
function attribute(key: string, value: string, privacy: Privacy, tag: string): string {
if (key === Constant.Href && tag === Constant.LinkTag) {
return key + "=" + value;
}
return key + "=" + scrub.text(value, key.indexOf(Constant.DataAttribute) === 0 ? Constant.DataAttribute : key, privacy);
}