clarity-js 0.6.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +26 -0
  2. package/build/clarity.js +4479 -0
  3. package/build/clarity.min.js +1 -0
  4. package/build/clarity.module.js +4473 -0
  5. package/package.json +66 -0
  6. package/rollup.config.ts +38 -0
  7. package/src/clarity.ts +54 -0
  8. package/src/core/config.ts +21 -0
  9. package/src/core/copy.ts +3 -0
  10. package/src/core/event.ts +25 -0
  11. package/src/core/hash.ts +19 -0
  12. package/src/core/history.ts +69 -0
  13. package/src/core/index.ts +79 -0
  14. package/src/core/measure.ts +17 -0
  15. package/src/core/report.ts +27 -0
  16. package/src/core/scrub.ts +102 -0
  17. package/src/core/task.ts +180 -0
  18. package/src/core/time.ts +14 -0
  19. package/src/core/timeout.ts +10 -0
  20. package/src/core/version.ts +2 -0
  21. package/src/data/baseline.ts +89 -0
  22. package/src/data/compress.ts +31 -0
  23. package/src/data/custom.ts +18 -0
  24. package/src/data/dimension.ts +42 -0
  25. package/src/data/encode.ts +109 -0
  26. package/src/data/envelope.ts +46 -0
  27. package/src/data/index.ts +43 -0
  28. package/src/data/limit.ts +42 -0
  29. package/src/data/metadata.ts +232 -0
  30. package/src/data/metric.ts +51 -0
  31. package/src/data/ping.ts +36 -0
  32. package/src/data/summary.ts +34 -0
  33. package/src/data/token.ts +39 -0
  34. package/src/data/upgrade.ts +36 -0
  35. package/src/data/upload.ts +250 -0
  36. package/src/data/variable.ts +46 -0
  37. package/src/diagnostic/encode.ts +40 -0
  38. package/src/diagnostic/image.ts +23 -0
  39. package/src/diagnostic/index.ts +14 -0
  40. package/src/diagnostic/internal.ts +41 -0
  41. package/src/diagnostic/script.ts +45 -0
  42. package/src/global.ts +22 -0
  43. package/src/index.ts +8 -0
  44. package/src/interaction/click.ts +140 -0
  45. package/src/interaction/encode.ts +140 -0
  46. package/src/interaction/index.ts +45 -0
  47. package/src/interaction/input.ts +64 -0
  48. package/src/interaction/pointer.ts +108 -0
  49. package/src/interaction/resize.ts +30 -0
  50. package/src/interaction/scroll.ts +73 -0
  51. package/src/interaction/selection.ts +66 -0
  52. package/src/interaction/timeline.ts +65 -0
  53. package/src/interaction/unload.ts +25 -0
  54. package/src/interaction/visibility.ts +24 -0
  55. package/src/layout/box.ts +83 -0
  56. package/src/layout/discover.ts +27 -0
  57. package/src/layout/document.ts +46 -0
  58. package/src/layout/dom.ts +442 -0
  59. package/src/layout/encode.ts +111 -0
  60. package/src/layout/extract.ts +75 -0
  61. package/src/layout/index.ts +25 -0
  62. package/src/layout/mutation.ts +232 -0
  63. package/src/layout/node.ts +211 -0
  64. package/src/layout/offset.ts +19 -0
  65. package/src/layout/region.ts +143 -0
  66. package/src/layout/schema.ts +66 -0
  67. package/src/layout/selector.ts +24 -0
  68. package/src/layout/target.ts +44 -0
  69. package/src/layout/traverse.ts +28 -0
  70. package/src/performance/connection.ts +37 -0
  71. package/src/performance/encode.ts +40 -0
  72. package/src/performance/index.ts +15 -0
  73. package/src/performance/navigation.ts +31 -0
  74. package/src/performance/observer.ts +87 -0
  75. package/test/core.test.ts +82 -0
  76. package/test/helper.ts +104 -0
  77. package/test/html/core.html +17 -0
  78. package/test/tsconfig.test.json +6 -0
  79. package/tsconfig.json +21 -0
  80. package/tslint.json +33 -0
  81. package/types/core.d.ts +127 -0
  82. package/types/data.d.ts +344 -0
  83. package/types/diagnostic.d.ts +24 -0
  84. package/types/index.d.ts +30 -0
  85. package/types/interaction.d.ts +110 -0
  86. package/types/layout.d.ts +200 -0
  87. package/types/performance.d.ts +40 -0
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "clarity-js",
3
+ "version": "0.6.23",
4
+ "description": "An analytics library that uses web page interactions to generate aggregated insights",
5
+ "author": "Microsoft Corp.",
6
+ "license": "MIT",
7
+ "main": "build/clarity.js",
8
+ "module": "build/clarity.module.js",
9
+ "unpkg": "build/clarity.min.js",
10
+ "types": "types/index.d.ts",
11
+ "keywords": [
12
+ "clarity",
13
+ "Microsoft",
14
+ "interactions",
15
+ "cursor",
16
+ "pointer",
17
+ "instrumentation",
18
+ "analytics"
19
+ ],
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/microsoft/clarity.git",
23
+ "directory": "packages/clarity-js"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/microsoft/clarity/issues"
27
+ },
28
+ "devDependencies": {
29
+ "@rollup/plugin-commonjs": "^19.0.1",
30
+ "@rollup/plugin-node-resolve": "^13.0.2",
31
+ "@types/chai": "^4.2.21",
32
+ "@types/mocha": "^8.2.3",
33
+ "@types/resize-observer-browser": "^0.1.6",
34
+ "chai": "^4.2.0",
35
+ "del-cli": "^4.0.1",
36
+ "husky": "^7.0.1",
37
+ "lint-staged": "^11.0.1",
38
+ "mocha": "^9.0.2",
39
+ "playwright": "^1.6.2",
40
+ "rollup": "^2.53.2",
41
+ "rollup-plugin-terser": "^7.0.2",
42
+ "rollup-plugin-typescript2": "^0.30.0",
43
+ "ts-mocha": "^8.0.0",
44
+ "tslib": "^2.3.0",
45
+ "tslint": "^6.1.3",
46
+ "typescript": "^4.3.5"
47
+ },
48
+ "scripts": {
49
+ "build": "yarn build:clean && yarn build:main",
50
+ "build:main": "rollup -c rollup.config.ts",
51
+ "build:clean": "del-cli build/*",
52
+ "test": "ts-mocha -p test/tsconfig.test.json test/**/*.test.ts",
53
+ "tslint": "tslint --project ./",
54
+ "tslint:fix": "tslint --fix --project ./ --force"
55
+ },
56
+ "husky": {
57
+ "hooks": {
58
+ "pre-commit": "lint-staged"
59
+ }
60
+ },
61
+ "lint-staged": {
62
+ "*.ts": [
63
+ "tslint --format codeFrame"
64
+ ]
65
+ }
66
+ }
@@ -0,0 +1,38 @@
1
+ import commonjs from "@rollup/plugin-commonjs";
2
+ import resolve from "@rollup/plugin-node-resolve";
3
+ import typescript from "rollup-plugin-typescript2";
4
+ import { terser } from "rollup-plugin-terser";
5
+ import pkg from "./package.json";
6
+
7
+ export default [
8
+ {
9
+ input: "src/index.ts",
10
+ output: [
11
+ { file: pkg.main, format: "cjs", exports: "named" },
12
+ { file: pkg.module, format: "es", exports: "named" }
13
+ ],
14
+ plugins: [
15
+ resolve(),
16
+ typescript({ rollupCommonJSResolveHack: true, clean: true }),
17
+ commonjs({ include: ["node_modules/**"] })
18
+ ],
19
+ onwarn(message, warn) {
20
+ if (message.code === 'CIRCULAR_DEPENDENCY') { return; }
21
+ warn(message);
22
+ }
23
+ },
24
+ {
25
+ input: "src/global.ts",
26
+ output: [ { file: pkg.unpkg, format: "iife", exports: "named" } ],
27
+ onwarn(message, warn) {
28
+ if (message.code === 'CIRCULAR_DEPENDENCY') { return; }
29
+ warn(message);
30
+ },
31
+ plugins: [
32
+ resolve(),
33
+ typescript({ rollupCommonJSResolveHack: true, clean: true }),
34
+ terser({output: {comments: false}}),
35
+ commonjs({ include: ["node_modules/**"] })
36
+ ]
37
+ }
38
+ ];
package/src/clarity.ts ADDED
@@ -0,0 +1,54 @@
1
+ import { Config, Module } from "@clarity-types/core";
2
+ import { Constant } from "@clarity-types/data";
3
+ import * as core from "@src/core";
4
+ import measure from "@src/core/measure";
5
+ import * as task from "@src/core/task";
6
+ import version from "@src/core/version";
7
+ import * as data from "@src/data";
8
+ import * as diagnostic from "@src/diagnostic";
9
+ import * as interaction from "@src/interaction";
10
+ import * as layout from "@src/layout";
11
+ import * as performance from "@src/performance";
12
+ export { version };
13
+ export { consent, event, identify, set, upgrade, metadata } from "@src/data";
14
+
15
+ const modules: Module[] = [diagnostic, layout, interaction, performance];
16
+
17
+ export function start(config: Config = null): void {
18
+ // Check that browser supports required APIs and we do not attempt to start Clarity multiple times
19
+ if (core.check()) {
20
+ core.config(config);
21
+ core.start();
22
+ data.start();
23
+ modules.forEach(x => measure(x.start)());
24
+ }
25
+ }
26
+
27
+ // By default Clarity is asynchronous and will yield by looking for requestIdleCallback.
28
+ // However, there can still be situations with single page apps where a user action can result
29
+ // in the whole DOM being destroyed and reconstructed. While Clarity will perform favorably out of the box,
30
+ // we do allow external clients to manually pause Clarity for that short burst of time and minimize
31
+ // performance impact even further. For reference, we are talking single digit milliseconds optimization here, not seconds.
32
+ export function pause(): void {
33
+ if (core.active()) {
34
+ data.event(Constant.Clarity, Constant.Pause);
35
+ task.pause();
36
+ }
37
+ }
38
+
39
+ // This is how external clients can get out of pause state, and resume Clarity to continue monitoring the page
40
+ export function resume(): void {
41
+ if (core.active()) {
42
+ task.resume();
43
+ data.event(Constant.Clarity, Constant.Resume);
44
+ }
45
+ }
46
+
47
+ export function stop(): void {
48
+ if (core.active()) {
49
+ // Stop modules in the reverse order of their initialization
50
+ modules.slice().reverse().forEach(x => measure(x.stop)());
51
+ data.stop();
52
+ core.stop();
53
+ }
54
+ }
@@ -0,0 +1,21 @@
1
+ import { Config, Time } from "@clarity-types/core";
2
+
3
+ let config: Config = {
4
+ projectId: null,
5
+ delay: 1 * Time.Second,
6
+ cssRules: false,
7
+ lean: false,
8
+ track: true,
9
+ content: true,
10
+ mask: [],
11
+ unmask: [],
12
+ regions: [],
13
+ metrics: [],
14
+ cookies: [],
15
+ report: null,
16
+ upload: null,
17
+ fallback: null,
18
+ upgrade: null
19
+ };
20
+
21
+ export default config;
@@ -0,0 +1,3 @@
1
+ export default function<T>(input: T): T {
2
+ return JSON.parse(JSON.stringify(input));
3
+ }
@@ -0,0 +1,25 @@
1
+ import { BrowserEvent } from "@clarity-types/core";
2
+ import measure from "./measure";
3
+
4
+ let bindings: BrowserEvent[] = [];
5
+
6
+ export function bind(target: EventTarget, event: string, listener: EventListener, capture: boolean = false): void {
7
+ listener = measure(listener) as EventListener;
8
+ // Wrapping following lines inside try / catch to cover edge cases where we might try to access an inaccessible element.
9
+ // E.g. Iframe may start off as same-origin but later turn into cross-origin, and the following lines will throw an exception.
10
+ try {
11
+ target.addEventListener(event, listener, capture);
12
+ bindings.push({ event, target, listener, capture });
13
+ } catch { /* do nothing */ }
14
+ }
15
+
16
+ export function reset(): void {
17
+ // Walk through existing list of bindings and remove them all
18
+ for (let binding of bindings) {
19
+ // Wrapping inside try / catch to avoid situations where the element may be destroyed before we get a chance to unbind
20
+ try {
21
+ binding.target.removeEventListener(binding.event, binding.listener, binding.capture);
22
+ } catch { /* do nothing */ }
23
+ }
24
+ bindings = [];
25
+ }
@@ -0,0 +1,19 @@
1
+ // tslint:disable: no-bitwise
2
+ export default function(input: string): string {
3
+ // Code inspired from C# GetHashCode: https://github.com/Microsoft/referencesource/blob/master/mscorlib/system/string.cs
4
+ let hash = 0;
5
+ let hashOne = 5381;
6
+ let hashTwo = hashOne;
7
+ for (let i = 0; i < input.length; i += 2) {
8
+ let charOne = input.charCodeAt(i);
9
+ hashOne = ((hashOne << 5) + hashOne) ^ charOne;
10
+ if (i + 1 < input.length) {
11
+ let charTwo = input.charCodeAt(i + 1);
12
+ hashTwo = ((hashTwo << 5) + hashTwo) ^ charTwo;
13
+ }
14
+ }
15
+ // Replace the magic number from C# implementation (1566083941) with a smaller prime number (11579)
16
+ // This ensures we don't hit integer overflow and prevent collisions
17
+ hash = Math.abs(hashOne + (hashTwo * 11579));
18
+ return hash.toString(36);
19
+ }
@@ -0,0 +1,69 @@
1
+ import { Code, Constant, Setting, Severity } from "@clarity-types/data";
2
+ import * as clarity from "@src/clarity";
3
+ import { bind } from "@src/core/event";
4
+ import * as internal from "@src/diagnostic/internal";
5
+
6
+ let pushState = null;
7
+ let replaceState = null;
8
+ let url = null;
9
+ let count = 0;
10
+
11
+ export function start(): void {
12
+ url = getCurrentUrl();
13
+ count = 0;
14
+ bind(window, "popstate", compute);
15
+
16
+ // Add a proxy to history.pushState function
17
+ if (pushState === null) { pushState = history.pushState; }
18
+ history.pushState = function(): void {
19
+ if (check()) {
20
+ pushState.apply(this, arguments);
21
+ compute();
22
+ }
23
+ };
24
+
25
+ // Add a proxy to history.replaceState function
26
+ if (replaceState === null) { replaceState = history.replaceState; }
27
+ history.replaceState = function(): void {
28
+ if (check()) {
29
+ replaceState.apply(this, arguments);
30
+ compute();
31
+ }
32
+ };
33
+ }
34
+
35
+ function check(): boolean {
36
+ if (count++ > Setting.CallStackDepth) {
37
+ internal.log(Code.CallStackDepth, Severity.Info);
38
+ return false;
39
+ }
40
+ return true;
41
+ }
42
+
43
+ function compute(): void {
44
+ if (url !== getCurrentUrl() && count <= Setting.CallStackDepth) {
45
+ clarity.stop();
46
+ window.setTimeout(clarity.start, Setting.RestartDelay);
47
+ }
48
+ }
49
+
50
+ function getCurrentUrl(): string {
51
+ return location.href ? location.href.replace(location.hash, Constant.Empty) : location.href;
52
+ }
53
+
54
+ export function stop(): void {
55
+ // Restore original function definition of history.pushState
56
+ if (pushState !== null) {
57
+ history.pushState = pushState;
58
+ pushState = null;
59
+ }
60
+
61
+ // Restore original function definition of history.replaceState
62
+ if (replaceState !== null) {
63
+ history.replaceState = replaceState;
64
+ replaceState = null;
65
+ }
66
+
67
+ url = null;
68
+ count = 0;
69
+ }
@@ -0,0 +1,79 @@
1
+ import { Config } from "@clarity-types/core";
2
+ import { Constant } from "@clarity-types/data";
3
+ import configuration from "@src/core/config";
4
+ import * as event from "@src/core/event";
5
+ import * as history from "@src/core/history";
6
+ import * as report from "@src/core/report";
7
+ import * as task from "@src/core/task";
8
+ import * as time from "@src/core/time";
9
+ import * as clarity from "@src/clarity";
10
+ import * as custom from "@src/data/custom";
11
+
12
+ let status = false;
13
+
14
+ export function start(): void {
15
+ status = true;
16
+ time.start();
17
+ task.reset();
18
+ event.reset();
19
+ report.reset();
20
+ history.start();
21
+ }
22
+
23
+ export function stop(): void {
24
+ history.stop();
25
+ report.reset();
26
+ event.reset();
27
+ task.reset();
28
+ time.stop();
29
+ status = false;
30
+ }
31
+
32
+ export function active(): boolean {
33
+ return status;
34
+ }
35
+
36
+ export function check(): boolean {
37
+ try {
38
+ return status === false &&
39
+ typeof Promise !== "undefined" &&
40
+ window["MutationObserver"] &&
41
+ document["createTreeWalker"] &&
42
+ "now" in Date &&
43
+ "now" in performance &&
44
+ typeof WeakMap !== "undefined";
45
+ } catch (ex) {
46
+ return false;
47
+ }
48
+ }
49
+
50
+ export function config(override: Config): boolean {
51
+ // Process custom configuration overrides, if available
52
+ if (override === null || status) { return false; }
53
+ for (let key in override) {
54
+ if (key in configuration) { configuration[key] = override[key]; }
55
+ }
56
+ return true;
57
+ }
58
+
59
+ // Suspend ends the current Clarity instance after a configured timeout period
60
+ // The way it differs from the "end" call is that it starts listening to
61
+ // user interaction events as soon as it terminates existing clarity instance.
62
+ // On the next interaction, it automatically starts another instance under a different page id
63
+ // E.g. if configured timeout is 10m, and user stays inactive for an hour.
64
+ // In this case, we will suspend clarity after 10m of inactivity and after another 50m when user interacts again
65
+ // Clarity will restart and start another instance seamlessly. Effectively not missing any active time, but also
66
+ // not holding the session during inactive time periods.
67
+ export function suspend(): void {
68
+ if (status) {
69
+ custom.event(Constant.Clarity, Constant.Suspend);
70
+ clarity.stop();
71
+ ["document", "touchstart"].forEach(x => event.bind(document, x, restart));
72
+ ["resize", "scroll", "pageshow"].forEach(x => event.bind(window, x, restart));
73
+ }
74
+ }
75
+
76
+ function restart(): void {
77
+ clarity.start();
78
+ custom.event(Constant.Clarity, Constant.Restart);
79
+ }
@@ -0,0 +1,17 @@
1
+ import { Setting } from "@clarity-types/core";
2
+ import { Metric } from "@clarity-types/data";
3
+ import * as metric from "@src/data/metric";
4
+
5
+ // tslint:disable-next-line: ban-types
6
+ export default function (method: Function): Function {
7
+ return function (): void {
8
+ let start = performance.now();
9
+ method.apply(this, arguments);
10
+ let duration = performance.now() - start;
11
+ metric.sum(Metric.TotalCost, duration);
12
+ if (duration > Setting.LongTask) {
13
+ metric.count(Metric.LongTaskCount);
14
+ metric.max(Metric.ThreadBlockedTime, duration);
15
+ }
16
+ };
17
+ }
@@ -0,0 +1,27 @@
1
+ import { Report } from "@clarity-types/core";
2
+ import { Check } from "@clarity-types/data";
3
+ import config from "@src/core/config";
4
+ import { data } from "@src/data/metadata";
5
+
6
+ let history: string[];
7
+
8
+ export function reset(): void {
9
+ history = [];
10
+ }
11
+
12
+ export function report(check: Check, message: string = null): void {
13
+ // Do not report the same message twice for the same page
14
+ if (history && history.indexOf(message) === -1) {
15
+ const url = config.report;
16
+ if (url && url.length > 0) {
17
+ let payload: Report = {c: check, p: data.projectId, u: data.userId, s: data.sessionId, n: data.pageNum };
18
+ if (message) payload.m = message;
19
+ // Using POST request instead of a GET request (img-src) to not violate existing CSP rules
20
+ // Since, Clarity already uses XHR to upload data, we stick with similar POST mechanism for reporting too
21
+ let xhr = new XMLHttpRequest();
22
+ xhr.open("POST", url);
23
+ xhr.send(JSON.stringify(payload));
24
+ history.push(message);
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,102 @@
1
+ import { Privacy } from "@clarity-types/core";
2
+ import * as Data from "@clarity-types/data";
3
+ import * as Layout from "@clarity-types/layout";
4
+
5
+ export default function(value: string, hint: string, privacy: Privacy, mangle: boolean = false): string {
6
+ if (value) {
7
+ switch (privacy) {
8
+ case Privacy.None:
9
+ return value;
10
+ case Privacy.Sensitive:
11
+ switch (hint) {
12
+ case Layout.Constant.TextTag:
13
+ case "value":
14
+ case "placeholder":
15
+ return redact(value);
16
+ case "input":
17
+ return mangleToken(value);
18
+ }
19
+ return value;
20
+ case Privacy.Text:
21
+ case Privacy.TextImage:
22
+ switch (hint) {
23
+ case Layout.Constant.TextTag:
24
+ return mangle ? mangleText(value) : mask(value);
25
+ case "src":
26
+ case "srcset":
27
+ case "title":
28
+ case "alt":
29
+ return privacy === Privacy.TextImage ? Data.Constant.Empty : value;
30
+ case "value":
31
+ case "click":
32
+ case "input":
33
+ return mangleToken(value);
34
+ case "placeholder":
35
+ return mask(value);
36
+ }
37
+ break;
38
+ }
39
+ }
40
+ return value;
41
+ }
42
+
43
+ function mangleText(value: string): string {
44
+ let trimmed = value.trim();
45
+ if (trimmed.length > 0) {
46
+ let first = trimmed[0];
47
+ let index = value.indexOf(first);
48
+ let prefix = value.substr(0, index);
49
+ let suffix = value.substr(index + trimmed.length);
50
+ return `${prefix}${trimmed.length.toString(36)}${suffix}`;
51
+ }
52
+ return value;
53
+ }
54
+
55
+ function mask(value: string): string {
56
+ return value.replace(/\S/gi, Data.Constant.Mask);
57
+ }
58
+
59
+ function mangleToken(value: string): string {
60
+ let length = ((Math.floor(value.length / Data.Setting.WordLength) + 1) * Data.Setting.WordLength);
61
+ let output: string = Layout.Constant.Empty;
62
+ for (let i = 0; i < length; i++) {
63
+ output += i > 0 && i % Data.Setting.WordLength === 0 ? Data.Constant.Space : Data.Constant.Mask;
64
+ }
65
+ return output;
66
+ }
67
+
68
+ function redact(value: string): string {
69
+ let spaceIndex = -1;
70
+ let hasDigit = false;
71
+ let hasEmail = false;
72
+ let hasWhitespace = false;
73
+ let array = null;
74
+ for (let i = 0; i < value.length; i++) {
75
+ let c = value.charCodeAt(i);
76
+ hasDigit = hasDigit || (c >= 48 && c <= 57); // Check for digits in the current word
77
+ hasEmail = hasEmail || c === 64; // Check for @ sign anywhere within the current word
78
+ hasWhitespace = c === 9 || c === 10 || c === 13 || c === 32; // Whitespace character (32: blank space | 9: \t | 10: \n | 13: \r)
79
+
80
+ // Process each word as an individual token to redact any sensitive information
81
+ if (i === 0 || i === value.length - 1 || hasWhitespace) {
82
+ // Performance optimization: Lazy load string -> array conversion only when required
83
+ if (hasDigit || hasEmail) {
84
+ if (array === null) { array = value.split(Data.Constant.Empty); }
85
+ mutate(array, spaceIndex, hasWhitespace ? i : i + 1);
86
+ }
87
+ // Reset digit and email flags after every word boundary, except the beginning of string
88
+ if (hasWhitespace) {
89
+ hasDigit = false;
90
+ hasEmail = false;
91
+ spaceIndex = i;
92
+ }
93
+ }
94
+ }
95
+ return array ? array.join(Data.Constant.Empty) : value;
96
+ }
97
+
98
+ function mutate(array: string[], start: number, end: number): void {
99
+ for (let i = start + 1; i < end; i++) {
100
+ array[i] = Data.Constant.Mask;
101
+ }
102
+ }