clarity-js 0.6.31 → 0.6.34

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.
@@ -1,6 +1,8 @@
1
1
  import { Priority, Task, Timer } from "@clarity-types/core";
2
2
  import { Code, Event, Metric, Severity } from "@clarity-types/data";
3
3
  import { Constant, MutationHistory, MutationQueue, Setting, Source } from "@clarity-types/layout";
4
+ import api from "@src/core/api";
5
+ import * as core from "@src/core";
4
6
  import { bind } from "@src/core/event";
5
7
  import measure from "@src/core/measure";
6
8
  import * as task from "@src/core/task";
@@ -34,32 +36,38 @@ export function start(): void {
34
36
  activePeriod = 0;
35
37
  history = {};
36
38
 
37
- if (insertRule === null) { insertRule = CSSStyleSheet.prototype.insertRule; }
38
- if (deleteRule === null) { deleteRule = CSSStyleSheet.prototype.deleteRule; }
39
- if (attachShadow === null) { attachShadow = Element.prototype.attachShadow; }
40
-
41
39
  // Some popular open source libraries, like styled-components, optimize performance
42
40
  // by injecting CSS using insertRule API vs. appending text node. A side effect of
43
41
  // using javascript API is that it doesn't trigger DOM mutation and therefore we
44
42
  // need to override the insertRule API and listen for changes manually.
45
- CSSStyleSheet.prototype.insertRule = function(): number {
46
- schedule(this.ownerNode);
47
- return insertRule.apply(this, arguments);
48
- };
49
-
50
- CSSStyleSheet.prototype.deleteRule = function(): void {
51
- schedule(this.ownerNode);
52
- return deleteRule.apply(this, arguments);
53
- };
43
+ if (insertRule === null) {
44
+ insertRule = CSSStyleSheet.prototype.insertRule;
45
+ CSSStyleSheet.prototype.insertRule = function(): number {
46
+ if (core.active()) { schedule(this.ownerNode); }
47
+ return insertRule.apply(this, arguments);
48
+ };
49
+ }
54
50
 
55
- // Add a hook to attachShadow API calls
56
- // In case we are unable to add a hook and browser throws an exception,
57
- // reset attachShadow variable and resume processing like before
58
- try {
59
- Element.prototype.attachShadow = function (): ShadowRoot {
60
- return schedule(attachShadow.apply(this, arguments)) as ShadowRoot;
61
- }
62
- } catch { attachShadow = null; }
51
+ if (deleteRule === null) {
52
+ deleteRule = CSSStyleSheet.prototype.deleteRule;
53
+ CSSStyleSheet.prototype.deleteRule = function(): void {
54
+ if (core.active()) { schedule(this.ownerNode); }
55
+ return deleteRule.apply(this, arguments);
56
+ };
57
+ }
58
+
59
+ // Add a hook to attachShadow API calls
60
+ // In case we are unable to add a hook and browser throws an exception,
61
+ // reset attachShadow variable and resume processing like before
62
+ if (attachShadow === null) {
63
+ attachShadow = Element.prototype.attachShadow;
64
+ try {
65
+ Element.prototype.attachShadow = function (): ShadowRoot {
66
+ if (core.active()) { return schedule(attachShadow.apply(this, arguments)) as ShadowRoot; }
67
+ else { return attachShadow.apply(this, arguments)}
68
+ }
69
+ } catch { attachShadow = null; }
70
+ }
63
71
  }
64
72
 
65
73
  export function observe(node: Node): void {
@@ -68,11 +76,8 @@ export function observe(node: Node): void {
68
76
  // For this reason, we need to wire up mutations every time we see a new shadow dom.
69
77
  // Also, wrap it inside a try / catch. In certain browsers (e.g. legacy Edge), observer on shadow dom can throw errors
70
78
  try {
71
- // In an edge case, it's possible to get stuck into infinite Mutation loop within Angular applications
72
- // This appears to be an issue with Zone.js package, see: https://github.com/angular/angular/issues/31712
73
- // As a temporary work around, ensuring Clarity can invoke MutationObserver outside of Zone (and use native implementation instead)
74
- let api: string = window[Constant.Zone] && Constant.Symbol in window[Constant.Zone] ? window[Constant.Zone][Constant.Symbol](Constant.MutationObserver) : Constant.MutationObserver;
75
- let observer = api in window ? new window[api](measure(handle) as MutationCallback) : null;
79
+ let m = api(Constant.MutationObserver);
80
+ let observer = m in window ? new window[m](measure(handle) as MutationCallback) : null;
76
81
  if (observer) {
77
82
  observer.observe(node, { attributes: true, childList: true, characterData: true, subtree: true });
78
83
  observers.push(observer);
@@ -92,25 +97,6 @@ export function monitor(frame: HTMLIFrameElement): void {
92
97
  export function stop(): void {
93
98
  for (let observer of observers) { if (observer) { observer.disconnect(); } }
94
99
  observers = [];
95
-
96
- // Restoring original insertRule
97
- if (insertRule !== null) {
98
- CSSStyleSheet.prototype.insertRule = insertRule;
99
- insertRule = null;
100
- }
101
-
102
- // Restoring original deleteRule
103
- if (deleteRule !== null) {
104
- CSSStyleSheet.prototype.deleteRule = deleteRule;
105
- deleteRule = null;
106
- }
107
-
108
- // Restoring original attachShadow
109
- if (attachShadow != null) {
110
- Element.prototype.attachShadow = attachShadow;
111
- attachShadow = null;
112
- }
113
-
114
100
  history = {};
115
101
  mutations = [];
116
102
  queue = [];
@@ -220,7 +206,7 @@ async function processNodeList(list: NodeList, source: Source, timer: Timer): Pr
220
206
  }
221
207
  }
222
208
 
223
- function schedule(node: Node): Node {
209
+ export function schedule(node: Node, fragment: boolean = false): Node {
224
210
  // Only schedule manual trigger for this node if it's not already in the queue
225
211
  if (queue.indexOf(node) < 0) { queue.push(node); }
226
212
 
@@ -228,17 +214,20 @@ function schedule(node: Node): Node {
228
214
  // It's common for a webpage to call multiple synchronous "insertRule" / "deleteRule" calls.
229
215
  // And in those cases we do not wish to monitor changes multiple times for the same node.
230
216
  if (timeout) { clearTimeout(timeout); }
231
- timeout = setTimeout(trigger, Setting.LookAhead);
217
+ timeout = setTimeout(() => { trigger(fragment) }, Setting.LookAhead);
232
218
 
233
219
  return node;
234
220
  }
235
221
 
236
- function trigger(): void {
222
+ function trigger(fragment: boolean): void {
237
223
  for (let node of queue) {
238
- let shadowRoot = node && node.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
239
- // Skip re-processing shadowRoot if it was already discovered
240
- if (shadowRoot && dom.has(node)) { continue; }
241
- generate(node, shadowRoot ? Constant.ChildList : Constant.CharacterData);
224
+ // Generate a mutation for this node only if it still exists
225
+ if (node) {
226
+ let shadowRoot = node.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
227
+ // Skip re-processing shadowRoot if it was already discovered
228
+ if (shadowRoot && dom.has(node)) { continue; }
229
+ generate(node, shadowRoot || fragment ? Constant.ChildList : Constant.CharacterData);
230
+ }
242
231
  }
243
232
  queue = [];
244
233
  }
package/types/core.d.ts CHANGED
@@ -3,9 +3,8 @@ import * as Data from "./data";
3
3
  type TaskFunction = () => Promise<void>;
4
4
  type TaskResolve = () => void;
5
5
  type UploadCallback = (data: string) => void;
6
- type Region = [number /* RegionId */, string /* Query Selector */, RegionFilter? /* Region Filter */, string? /* Filter Text */];
7
- type Metric = [Data.Metric /* MetricId */, Extract /* Extract Filter */, string /* Match Value */, number? /* Scale Factor */];
8
- type Dimension = [Data.Dimension /* DimensionId */, Extract /* Extract Filter */, string /* Match Value */];
6
+ type Region = [number /* RegionId */, string /* Query Selector */];
7
+ export type Extract = ExtractSource /* Extraction Source */ | number /* Extract Id */ | string | string[] /* Hash or Query Selector or String Token */;
9
8
 
10
9
  /* Enum */
11
10
 
@@ -21,7 +20,6 @@ export const enum Time {
21
20
  Day = 24 * 60 * 60 * 1000
22
21
  }
23
22
 
24
-
25
23
  export const enum Task {
26
24
  Wait = 0,
27
25
  Run = 1,
@@ -32,15 +30,23 @@ export const enum Setting {
32
30
  LongTask = 30, // 30ms
33
31
  }
34
32
 
35
- export const enum RegionFilter {
36
- Url = 0,
37
- Javascript = 1
33
+ export const enum ExtractSource {
34
+ Javascript = 0,
35
+ Cookie = 1,
36
+ Text = 2,
37
+ Fragment = 3
38
+ }
39
+
40
+ export const enum Type {
41
+ Array = 1,
42
+ Object = 2,
43
+ Simple = 3
38
44
  }
39
45
 
40
- export const enum Extract {
41
- Text = 0,
42
- Javascript = 1,
43
- Attribute = 2
46
+ export type Syntax = {
47
+ name: string,
48
+ type: Type,
49
+ condition: string
44
50
  }
45
51
 
46
52
  export const enum Privacy {
@@ -119,11 +125,17 @@ export interface Config {
119
125
  mask?: string[];
120
126
  unmask?: string[];
121
127
  regions?: Region[];
122
- metrics?: Metric[];
123
- dimensions?: Dimension[];
128
+ extract?: Extract[];
124
129
  cookies?: string[];
125
130
  report?: string;
126
131
  upload?: string | UploadCallback;
127
132
  fallback?: string;
128
133
  upgrade?: (key: string) => void;
129
134
  }
135
+
136
+ export const enum Constant {
137
+ Zone = "Zone",
138
+ Symbol = "__symbol__",
139
+ AddEventListener = "addEventListener",
140
+ RemoveEventListener = "removeEventListener"
141
+ }
package/types/data.d.ts CHANGED
@@ -1,12 +1,16 @@
1
1
  import { Time } from "@clarity-types/core";
2
+ import { callback } from "@src/data/metadata";
2
3
  export type Target = (number | Node);
3
4
  export type Token = (string | number | number[] | string[]);
4
5
  export type DecodedToken = (any | any[]);
5
6
 
6
7
  export type MetadataCallback = (data: Metadata, playback: boolean) => void;
8
+ export interface MetadataCallbackOptions {
9
+ callback: MetadataCallback,
10
+ wait: boolean
11
+ }
7
12
 
8
13
  /* Enum */
9
-
10
14
  export const enum Event {
11
15
  /* Data */
12
16
  Metric = 0,
@@ -54,7 +58,8 @@ export const enum Event {
54
58
  Summary = 36,
55
59
  Box = 37,
56
60
  Clipboard = 38,
57
- Submit = 39
61
+ Submit = 39,
62
+ Extract = 40
58
63
  }
59
64
 
60
65
  export const enum Metric {
@@ -135,7 +140,11 @@ export const enum Code {
135
140
  CallStackDepth = 4,
136
141
  Selector = 5,
137
142
  Metric = 6,
138
- ContentSecurityPolicy = 7
143
+ /**
144
+ * @deprecated No longer support ContentSecurityPolicy
145
+ */
146
+ ContentSecurityPolicy = 7,
147
+ Config = 8
139
148
  }
140
149
 
141
150
  export const enum Severity {
@@ -184,7 +193,8 @@ export const enum Setting {
184
193
  MaxFirstPayloadBytes = 1 * 1024 * 1024, // 1MB: Cap the very first payload to a maximum of 1MB
185
194
  UploadFactor = 3, // Slow down sequence by specified factor
186
195
  MinUploadDelay = 100, // Minimum time before we are ready to flush events to the server
187
- MaxUploadDelay = 30 * Time.Second // Do flush out payload once every 30s
196
+ MaxUploadDelay = 30 * Time.Second, // Do flush out payload once every 30s,
197
+ ExtractLimit = 10000 // Do not extract more than 10000 characters
188
198
  }
189
199
 
190
200
  export const enum Character {
@@ -245,6 +255,9 @@ export const enum Constant {
245
255
  Accept = "Accept",
246
256
  ClarityGzip = "application/x-clarity-gzip",
247
257
  Tilde = "~",
258
+ ArrayStart = "[",
259
+ ConditionStart = "{",
260
+ ConditionEnd = "}"
248
261
  }
249
262
 
250
263
  export const enum XMLReadyState {
@@ -363,6 +376,10 @@ export interface UpgradeData {
363
376
  key: string;
364
377
  }
365
378
 
379
+ export interface ExtractData {
380
+ [key: string]: string | number;
381
+ }
382
+
366
383
  export interface UploadData {
367
384
  sequence: number;
368
385
  attempts: number;
package/types/layout.d.ts CHANGED
@@ -75,8 +75,6 @@ export const enum Constant {
75
75
  BorderBox = "border-box",
76
76
  Value = "value",
77
77
  MutationObserver = "MutationObserver",
78
- Zone = "Zone",
79
- Symbol = "__symbol__",
80
78
  JsonLD = "application/ld+json",
81
79
  String = "string",
82
80
  Number = "number",
@@ -156,6 +154,7 @@ export interface NodeValue {
156
154
  hash: [string, string];
157
155
  region: number;
158
156
  metadata: NodeMeta;
157
+ fragment: number;
159
158
  }
160
159
 
161
160
  export interface NodeMeta {
@@ -1,94 +0,0 @@
1
- import { Dimension, Extract, Metric, Region, RegionFilter } from "@clarity-types/core";
2
- import { Constant, Setting } from "@clarity-types/data";
3
- import * as dimension from "@src/data/dimension";
4
- import * as metric from "@src/data/metric";
5
- import * as region from "@src/layout/region";
6
-
7
- const formatRegex = /1/g;
8
- const digitsRegex = /[^0-9\.]/g;
9
- const digitsWithCommaRegex = /[^0-9\.,]/g;
10
- const regexCache: {[key: string]: RegExp} = {};
11
-
12
- export function regions(root: ParentNode, value: Region[]): void {
13
- for (let v of value) {
14
- const [regionId, selector, filter, match] = v;
15
- let valid = true;
16
- switch (filter) {
17
- case RegionFilter.Url: valid = match && !!top.location.href.match(regex(match)); break;
18
- case RegionFilter.Javascript: valid = match && !!evaluate(match); break;
19
- }
20
- if (valid) { root.querySelectorAll(selector).forEach(e => region.observe(e, regionId.toString())); }
21
- }
22
- }
23
-
24
- export function metrics(root: ParentNode, value: Metric[]): void {
25
- for (let v of value) {
26
- const [metricId, source, match, scale] = v;
27
- if (match) {
28
- switch (source) {
29
- case Extract.Text: root.querySelectorAll(match).forEach(e => { metric.max(metricId, num((e as HTMLElement).innerText, scale)); }); break;
30
- case Extract.Attribute: root.querySelectorAll(`[${match}]`).forEach(e => { metric.max(metricId, num(e.getAttribute(match), scale, false)); }); break;
31
- case Extract.Javascript: metric.max(metricId, evaluate(match, Constant.Number) as number); break;
32
- }
33
- }
34
- }
35
- }
36
-
37
- export function dimensions(root: ParentNode, value: Dimension[]): void {
38
- for (let v of value) {
39
- const [dimensionId, source, match] = v;
40
- if (match) {
41
- switch (source) {
42
- case Extract.Text: root.querySelectorAll(match).forEach(e => { dimension.log(dimensionId, str((e as HTMLElement).innerText)); }); break;
43
- case Extract.Attribute: root.querySelectorAll(`[${match}]`).forEach(e => { dimension.log(dimensionId, str(e.getAttribute(match))); }); break;
44
- case Extract.Javascript: dimension.log(dimensionId, str(evaluate(match, Constant.String))); break;
45
- }
46
- }
47
- }
48
- }
49
-
50
- function regex(match: string): RegExp {
51
- regexCache[match] = match in regexCache ? regexCache[match] : new RegExp(match);
52
- return regexCache[match];
53
- }
54
-
55
- // The function below takes in a variable name in following format: "a.b.c" and safely evaluates its value in javascript context
56
- // For instance, for a.b.c, it will first check window["a"]. If it exists, it will recursively look at: window["a"]["b"] and finally,
57
- // return the value for window["a"]["b"]["c"].
58
- function evaluate(variable: string, type: string = null, base: Object = window): any {
59
- let parts = variable.split(Constant.Dot);
60
- let first = parts.shift();
61
- if (base && base[first]) {
62
- if (parts.length > 0) { return evaluate(parts.join(Constant.Dot), type, base[first]); }
63
- let output = type === null || type === typeof base[first] ? base[first] : null;
64
- return output;
65
- }
66
- return null;
67
- }
68
-
69
- function str(input: string): string {
70
- // Automatically trim string to max of Setting.DimensionLimit to avoid fetching long strings
71
- return input ? input.substr(0, Setting.DimensionLimit) : input;
72
- }
73
-
74
- function num(text: string, scale: number, localize: boolean = true): number {
75
- try {
76
- scale = scale || 1;
77
- // Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat
78
- let lang = document.documentElement.lang;
79
- if (Intl && Intl.NumberFormat && lang && localize) {
80
- text = text.replace(digitsWithCommaRegex, Constant.Empty);
81
- // Infer current group and decimal separator from current locale
82
- let group = Intl.NumberFormat(lang).format(11111).replace(formatRegex, Constant.Empty);
83
- let decimal = Intl.NumberFormat(lang).format(1.1).replace(formatRegex, Constant.Empty);
84
-
85
- // Parse number using inferred group and decimal separators
86
- return Math.round(parseFloat(text
87
- .replace(new RegExp('\\' + group, 'g'), Constant.Empty)
88
- .replace(new RegExp('\\' + decimal), Constant.Dot)
89
- ) * scale);
90
- }
91
- // Fallback to en locale
92
- return Math.round(parseFloat(text.replace(digitsRegex, Constant.Empty)) * scale);
93
- } catch { return null; }
94
- }