--- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,3 +1,4 @@ +import * as sitePolicy from '@subneat/policy'; import { Config } from "@clarity-types/core"; import { Constant } from "@clarity-types/data"; import configuration from "@src/core/config"; @@ -36,7 +37,7 @@ export function check(config: Config = null): boolean { try { let globalPrivacyControlSet = navigator && "globalPrivacyControl" in navigator && navigator['globalPrivacyControl'] == true; - return status === false && + return sitePolicy.knownPage() && status === false && typeof Promise !== "undefined" && window["MutationObserver"] && document["createTreeWalker"] && --- a/src/core/scrub.ts +++ b/src/core/scrub.ts @@ -1,3 +1,4 @@ +import * as sitePolicy from '@subneat/policy'; import { Privacy } from "@clarity-types/core"; import * as Data from "@clarity-types/data"; import * as Layout from "@clarity-types/layout"; @@ -11,6 +12,9 @@ let currencyRegex = null; export function text(value: string, hint: string, privacy: Privacy, mangle: boolean = false, type?: string): string { + if (hint === "checksum") return ""; + if (["input","change","value"].includes(hint)) return value ? mangleToken(value) : value; + if (privacy === Privacy.None && !sitePolicy.publicCopy(value)) privacy = Privacy.TextImage; if (value) { if (hint == "input" && (type === "checkbox" || type === "radio")) { return value; @@ -94,6 +98,7 @@ } export function url(input: string, electron: boolean = false, truncate: boolean = false): string { + return sitePolicy.safeUrl(input); let result = input; // Replace the URL for Electron apps so we don't send back file:/// URL if (electron) { --- a/src/data/dimension.ts +++ b/src/data/dimension.ts @@ -1,3 +1,4 @@ +import * as sitePolicy from '@subneat/policy'; import { Check, Event, Dimension, DimensionData, Setting } from "@clarity-types/data"; import * as limit from "@src/data/limit"; import encode from "./encode"; @@ -19,6 +20,7 @@ } export function log(dimension: Dimension, value: string): void { + value = sitePolicy.dimensionValue(dimension, value); // Check valid value before moving ahead if (value) { // Ensure received value is casted into a string if it wasn't a string to begin with --- a/src/data/metric.ts +++ b/src/data/metric.ts @@ -1,3 +1,4 @@ +import * as sitePolicy from '@subneat/policy'; import { Event, Metric, MetricData } from "@clarity-types/data"; import encode from "./encode"; @@ -21,12 +22,14 @@ } export function count(metric: Metric): void { + if (!sitePolicy.metricAllowed(metric)) return; init(metric); data[metric]++; updates[metric]++; } export function sum(metric: Metric, value: number): void { + if (!sitePolicy.metricAllowed(metric)) return; if (value !== null) { init(metric); data[metric] += value; @@ -35,6 +38,7 @@ } export function max(metric: Metric, value: number): void { + if (!sitePolicy.metricAllowed(metric)) return; // Ensure that we do not process null or NaN values if (value !== null && isNaN(value) === false) { if (!(metric in data)) { data[metric] = 0; } --- a/src/data/upload.ts +++ b/src/data/upload.ts @@ -1,3 +1,4 @@ +import * as sitePolicy from '@subneat/policy'; import { UploadCallback } from "@clarity-types/core"; import { BooleanFlag, Check, Code, Constant, EncodedPayload, Event, Metric, Setting, Severity, Token, Transit, UploadData, XMLReadyState } from "@clarity-types/data"; import * as clarity from "@src/clarity"; @@ -53,6 +54,8 @@ let now = time(); let type = tokens.length > 1 ? tokens[1] : null; + if (!sitePolicy.eventAllowed(Number(type))) return; + if (type === Event.Navigation) tokens = tokens.map((value, index) => index >= 12 ? (typeof value === 'number' ? 0 : '') : value); let event = JSON.stringify(tokens); if (!config.lean) { --- a/src/data/baseline.ts +++ b/src/data/baseline.ts @@ -48,6 +48,7 @@ } export function track(event: Event, x: number, y: number, time?: number): void { + if (![Event.Document, Event.Resize, Event.Scroll].includes(event)) return; switch (event) { case Event.Document: buffer.docWidth = x; --- a/src/data/summary.ts +++ b/src/data/summary.ts @@ -1,3 +1,4 @@ +import * as sitePolicy from '@subneat/policy'; import { Event, SummaryData, Setting } from "@clarity-types/data"; import encode from "./encode"; @@ -12,6 +13,7 @@ } export function track(event: Event, time: number): void { + if (!sitePolicy.eventAllowed(event)) return; if (!(event in data)) { data[event] = [[time, 0]]; } else { --- a/src/data/custom.ts +++ b/src/data/custom.ts @@ -1,3 +1,4 @@ +import * as sitePolicy from '@subneat/policy'; import { Constant, CustomData, Event } from "@clarity-types/data"; import * as core from "@src/core"; import encode from "./encode"; @@ -7,7 +8,7 @@ // custom events allow 2 parameters or 1 parameter to be passed. If 2 are passed we // consider it a key value pair. If only 1 is passed we only consider the event to have a value. export function event(a: string, b: string): void { - if (core.active() && + if (sitePolicy.customEventAllowed(a, b) && core.active() && a && typeof a === Constant.String && a.length < 255 --- a/src/performance/observer.ts +++ b/src/performance/observer.ts @@ -74,7 +74,7 @@ if (visible) { metric.max(Metric.FirstInputDelay, entry["processingStart"] - entry.startTime); } break; case Constant.PerformanceEventTiming: - if (visible && 'PerformanceEventTiming' in window && 'interactionId' in PerformanceEventTiming.prototype) + if (visible && 'PerformanceEventTiming' in window && 'interactionId' in (window as any).PerformanceEventTiming.prototype) { interaction.processInteractionEntry(entry as PerformanceEventTiming); // Logging it as dimension because we're always looking for the last value. --- a/src/data/metadata.ts +++ b/src/data/metadata.ts @@ -1,3 +1,4 @@ +import * as sitePolicy from '@subneat/policy'; import { Constant as CoreConstant, Time } from "@clarity-types/core"; import { BooleanFlag, ConsentData, ConsentSource, ConsentState, Constant, Dimension, Metadata, MetadataCallback, MetadataCallbackOptions, Metric, Session, Setting, User } from "@clarity-types/data"; import * as clarity from "@src/clarity"; @@ -20,6 +21,8 @@ let defaultStatus: ConsentState = { source: ConsentSource.Default, ad_Storage: Constant.Denied, analytics_Storage: Constant.Denied }; export function start(): void { + config.track = sitePolicy.siteAnalyticsStorage(); + consentStatus = {source: ConsentSource.APIv2, ad_Storage: Constant.Denied, analytics_Storage: config.track ? Constant.Granted : Constant.Denied}; const ua = navigator && "userAgent" in navigator ? navigator.userAgent : Constant.Empty; const timezone = (typeof Intl !== 'undefined' && Intl?.DateTimeFormat()?.resolvedOptions()?.timeZone) ?? ''; const timezoneOffset = "" + new Date().getTimezoneOffset(); @@ -177,8 +180,8 @@ const updatedStatus = { source: consentState.source ?? source, - ad_Storage: normalizeConsent(consentState.ad_Storage, consentStatus?.ad_Storage), - analytics_Storage: normalizeConsent(consentState.analytics_Storage, consentStatus?.analytics_Storage), + ad_Storage: Constant.Denied, + analytics_Storage: sitePolicy.siteAnalyticsStorage() ? Constant.Granted : Constant.Denied, }; if ( @@ -309,7 +312,7 @@ function session(): Session { let output: Session = { session: shortid(), ts: Math.round(Date.now()), count: 1, upgrade: null, upload: Constant.Empty }; - let value = getCookie(Constant.SessionKey, !config.includeSubdomains); + let value = config.track ? getCookie(Constant.SessionKey, !config.includeSubdomains) : null; if (value) { // Maintaining support for pipe separator for backward compatibility, this can be removed in future releases let parts = value.includes(Constant.Caret) ? value.split(Constant.Caret) : value.split(Constant.Pipe); @@ -332,7 +335,7 @@ function user(): User { let output: User = { id: shortid(), version: 0, expiry: null, consent: BooleanFlag.False, dob: 0 }; - let cookie = getCookie(Constant.CookieKey, !config.includeSubdomains); + let cookie = config.track ? getCookie(Constant.CookieKey, !config.includeSubdomains) : null; if (cookie && cookie.length > 0) { // Splitting and looking up first part for forward compatibility, in case we wish to store additional information in a cookie // Maintaining support for pipe separator for backward compatibility, this can be removed in future releases --- a/src/layout/node.ts +++ b/src/layout/node.ts @@ -1,3 +1,4 @@ +import * as sitePolicy from '@subneat/policy'; import { Constant, Source } from "@clarity-types/layout"; import { Code, Dimension, Severity } from "@clarity-types/data"; import { @@ -19,6 +20,7 @@ export default function (node: Node, source: Source, timestamp: number): Node { let child: Node = null; + if (sitePolicy.privateDescendant(node)) return child; // Do not track this change if we are attempting to remove a node before discovering it if (source === Source.ChildListRemove && domHas(node) === false) { return child; } @@ -93,7 +95,8 @@ case Node.ELEMENT_NODE: let element = (node as HTMLElement); let tag = element.tagName; - let attributes = getAttributes(element); + if (!sitePolicy.knownTag(tag)) return child; + let attributes = sitePolicy.safeAttributes(getAttributes(element)); // In some cases, external libraries like vue-fragment, can modify parentNode property to not be in sync with the DOM // For correctness, we first look at parentElement and if it not present then fall back to using parentNode parent = node.parentElement ? node.parentElement : (node.parentNode ? node.parentNode as HTMLElement : null); --- a/src/interaction/encode.ts +++ b/src/interaction/encode.ts @@ -1,3 +1,4 @@ +import * as sitePolicy from '@subneat/policy'; import { Constant, Event, Token } from "@clarity-types/data"; import * as scrub from "@src/core/scrub"; import { time } from "@src/core/time"; @@ -59,6 +60,7 @@ case Event.ContextMenu: for (let entry of click.state) { let cTarget = metadata(entry.data.target as Node, entry.event, entry.data.text); + if (cTarget.id <= 0) continue; tokens = [entry.time, entry.event]; let cHash = cTarget.hash ? cTarget.hash.join(Constant.Dot) : Constant.Empty; tokens.push( @@ -69,7 +71,7 @@ scrub.url(entry.data.link), cHash, entry.data.trust, entry.data.isFullText, entry.data.w, entry.data.h, - entry.data.tag, entry.data.class, entry.data.id, entry.data.source + sitePolicy.knownTag((entry.data.tag || "").toUpperCase()) ? entry.data.tag : "", sitePolicy.fixedClasses(entry.data.class || ""), sitePolicy.fixedId(entry.data.id || ""), 0 ); queue(tokens); timeline.track(entry.time, entry.event, cHash, entry.data.x, entry.data.y, entry.data.reaction, entry.data.context); --- a/src/layout/encode.ts +++ b/src/layout/encode.ts @@ -1,3 +1,4 @@ +import * as sitePolicy from '@subneat/policy'; import { Privacy, Task, Timer } from "@clarity-types/core"; import { Event, Setting, Token } from "@clarity-types/data"; import { Constant, NodeInfo, NodeValue } from "@clarity-types/layout"; @@ -94,9 +95,10 @@ 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)); + let permitted = sitePolicy.safeAttributes(data[key]); + for (let attr in permitted) { + if (permitted[attr] !== undefined) { + tokens.push(attribute(attr, permitted[attr], privacy, data.tag)); } } break; --- a/src/diagnostic/encode.ts +++ b/src/diagnostic/encode.ts @@ -16,7 +16,7 @@ break; case Event.Log: if (internal.data) { - tokens.push(internal.data.code, internal.data.name, internal.data.message, internal.data.stack, internal.data.severity); + tokens.push(internal.data.code, '', '', '', internal.data.severity); queue(tokens, false); } break; --- a/src/diagnostic/script.ts +++ b/src/diagnostic/script.ts @@ -1,35 +1,8 @@ -import { Event, Setting } from "@clarity-types/data"; -import { ScriptErrorData } from "@clarity-types/diagnostic"; -import { bind } from "@src/core/event"; -import encode from "./encode"; - -let history: { [key: string]: number } = {}; -export let data: ScriptErrorData; - -export function start(): void { - bind(window, "error", handler); - history = {}; -} - -function handler(error: ErrorEvent): boolean { - let e = error["error"] || error; - // While rare, it's possible for code to fail repeatedly during the lifetime of the same page - // In those cases, we only want to log the failure first few times and not spam logs with redundant information. - if (!(e.message in history)) { history[e.message] = 0; } - if (history[e.message]++ >= Setting.ScriptErrorLimit) { return true; } - - // Send back information only if the handled error has valid information - if (e && e.message) { - data = { - message: e.message, - line: error["lineno"], - column: error["colno"], - stack: e.stack, - source: error["filename"] - }; - - encode(Event.ScriptError); - } - - return true; +// Site profile: fixed categories only; never read error messages or stacks. +import {bind} from '@src/core/event'; +import {event} from '@src/data/custom'; +export let data = null; +export function start(){ + bind(window,'error',(error: ErrorEvent)=>{event(error instanceof ErrorEvent?'script_error':'resource_error','1');return true;}); + bind(window,'unhandledrejection',()=>{event('unhandled_rejection','1');}); }