clarity-js
Version:
An analytics library that uses web page interactions to generate aggregated insights
202 lines (187 loc) • 8.08 kB
text/typescript
import { Privacy } from "@clarity-types/core";
import * as Data from "@clarity-types/data";
import * as Layout from "@clarity-types/layout";
import config from "@src/core/config";
const catchallRegex = /\S/gi;
const maxUrlLength = 2048;
let unicodeRegex = true;
let digitRegex = null;
let letterRegex = null;
let currencyRegex = null;
export function text(value: string, hint: string, privacy: Privacy, mangle: boolean = false, type?: string): string {
if (value) {
if (hint == "input" && (type === "checkbox" || type === "radio")) {
return value;
}
switch (privacy) {
case Privacy.None:
return value;
case Privacy.Sensitive:
switch (hint) {
case Layout.Constant.TextTag:
case "value":
case "placeholder":
case "click":
return redact(value);
case "input":
case "change":
return mangleToken(value);
}
return value;
case Privacy.Text:
case Privacy.TextImage:
switch (hint) {
case Layout.Constant.TextTag:
case Layout.Constant.DataAttribute:
return mangle ? mangleText(value) : mask(value);
case "src":
case "srcset":
case "title":
case "alt":
case "href":
case "xlink:href":
if (privacy === Privacy.TextImage) {
return value?.startsWith('blob:') ? 'blob:' : Data.Constant.Empty;
}
return value;
case "value":
case "click":
case "input":
case "change":
return mangleToken(value);
case "placeholder":
return mask(value);
}
break;
case Privacy.Exclude:
switch (hint) {
case Layout.Constant.TextTag:
case Layout.Constant.DataAttribute:
return mangle ? mangleText(value) : mask(value);
case "value":
case "input":
case "click":
case "change":
return Array(Data.Setting.WordLength).join(Data.Constant.Mask);
case "checksum":
return Data.Constant.Empty;
}
break;
case Privacy.Snapshot:
switch (hint) {
case Layout.Constant.TextTag:
case Layout.Constant.DataAttribute:
return scrub(value, Data.Constant.Letter, Data.Constant.Digit);
case "value":
case "input":
case "click":
case "change":
return Array(Data.Setting.WordLength).join(Data.Constant.Mask);
case "checksum":
case "src":
case "srcset":
case "alt":
case "title":
return Data.Constant.Empty;
}
break;
}
}
return value;
}
export function url(input: string, electron: boolean = false, truncate: boolean = false): string {
let result = input;
// Replace the URL for Electron apps so we don't send back file:/// URL
if (electron) {
result = Data.Constant.HTTPS + Data.Constant.Electron;
} else {
let drop = config.drop;
if (drop && drop.length > 0 && input && input.indexOf("?") > 0) {
let [path, query] = input.split("?");
let swap = Data.Constant.Dropped;
result = path + "?" + query.split("&").map(p => drop.some(x => p.indexOf(x + "=") === 0) ? (p.split("=")[0] + "=" + swap) : p).join("&");
}
}
if (truncate) {
result = result.substring(0, maxUrlLength);
}
return result;
}
function mangleText(value: string): string {
let trimmed = value.trim();
if (trimmed.length > 0) {
let first = trimmed[0];
let index = value.indexOf(first);
let prefix = value.substr(0, index);
let suffix = value.substr(index + trimmed.length);
return prefix + trimmed.length.toString(36) + suffix;
}
return value;
}
function mask(value: string): string {
return value.replace(catchallRegex, Data.Constant.Mask);
}
export function scrub(value: string, letter: string, digit: string): string {
regex(); // Initialize regular expressions
return value ? value.replace(letterRegex, letter).replace(digitRegex, digit) : value;
}
function mangleToken(value: string): string {
let length = ((Math.floor(value.length / Data.Setting.WordLength) + 1) * Data.Setting.WordLength);
let output: string = Layout.Constant.Empty;
for (let i = 0; i < length; i++) {
output += i > 0 && i % Data.Setting.WordLength === 0 ? Data.Constant.Space : Data.Constant.Mask;
}
return output;
}
function regex(): void {
// Initialize unicode regex, if supported by the browser
// Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Unicode_Property_Escapes
if (unicodeRegex && digitRegex === null) {
try {
digitRegex = new RegExp("\\p{N}", "gu");
letterRegex = new RegExp("\\p{L}", "gu");
currencyRegex = new RegExp("\\p{Sc}", "gu");
} catch { unicodeRegex = false; }
}
}
function redact(value: string): string {
let spaceIndex = -1;
let gap = 0;
let hasDigit = false;
let hasEmail = false;
let hasWhitespace = false;
let array = null;
regex(); // Initialize regular expressions
for (let i = 0; i < value.length; i++) {
let c = value.charCodeAt(i);
hasDigit = hasDigit || (c >= Data.Character.Zero && c <= Data.Character.Nine); // Check for digits in the current word
hasEmail = hasEmail || c === Data.Character.At; // Check for @ sign anywhere within the current word
hasWhitespace = c === Data.Character.Tab || c === Data.Character.NewLine || c === Data.Character.Return || c === Data.Character.Blank;
// Process each word as an individual token to redact any sensitive information
if (i === 0 || i === value.length - 1 || hasWhitespace) {
// Performance optimization: Lazy load string -> array conversion only when required
if (hasDigit || hasEmail) {
if (array === null) { array = value.split(Data.Constant.Empty); }
// Work on a token at a time so we don't have to apply regex to a larger string
let token = value.substring(spaceIndex + 1, hasWhitespace ? i : i + 1);
// Check if unicode regex is supported, otherwise fallback to calling mask function on this token
if (unicodeRegex && currencyRegex !== null) {
// Do not redact information if the token contains a currency symbol
token = token.match(currencyRegex) ? token : scrub(token, Data.Constant.Letter, Data.Constant.Digit);
} else {
token = mask(token);
}
// Merge token back into array at the right place
array.splice(spaceIndex + 1 - gap, token.length, token);
gap += token.length - 1;
}
// Reset digit and email flags after every word boundary, except the beginning of string
if (hasWhitespace) {
hasDigit = false;
hasEmail = false;
spaceIndex = i;
}
}
}
return array ? array.join(Data.Constant.Empty) : value;
}