clarity-js 0.7.3 → 0.7.5

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clarity-js",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
4
4
  "description": "An analytics library that uses web page interactions to generate aggregated insights",
5
5
  "author": "Microsoft Corp.",
6
6
  "license": "MIT",
@@ -10,7 +10,6 @@ let config: Config = {
10
10
  mask: [],
11
11
  unmask: [],
12
12
  regions: [],
13
- extract: [],
14
13
  cookies: [],
15
14
  fraud: true,
16
15
  checksum: [],
@@ -19,7 +19,7 @@ export function report(e: Error): Error {
19
19
  // Using POST request instead of a GET request (img-src) to not violate existing CSP rules
20
20
  // Since, Clarity already uses XHR to upload data, we stick with similar POST mechanism for reporting too
21
21
  let xhr = new XMLHttpRequest();
22
- xhr.open("POST", url);
22
+ xhr.open("POST", url, true);
23
23
  xhr.send(JSON.stringify(payload));
24
24
  history.push(e.message);
25
25
  }
@@ -1,2 +1,2 @@
1
- let version = "0.7.3";
1
+ let version = "0.7.5";
2
2
  export default version;
@@ -110,7 +110,7 @@ export default function(event: Event): void {
110
110
  let extractKeys = extract.keys;
111
111
  for (let e of extractKeys) {
112
112
  tokens.push(e);
113
- tokens.push(extract.data[e]);
113
+ tokens.push([].concat(...extract.data[e]));
114
114
  }
115
115
  extract.reset();
116
116
  queue(tokens, false);
@@ -1,38 +1,36 @@
1
1
  import { ExtractSource, Syntax, Type } from "@clarity-types/core";
2
2
  import { Event, Setting, ExtractData } from "@clarity-types/data";
3
- import config from "@src/core/config";
4
3
  import encode from "./encode";
5
4
  import * as internal from "@src/diagnostic/internal";
6
5
  import { Code, Constant, Severity } from "@clarity-types/data";
7
6
 
8
7
  export let data: ExtractData = {};
9
- export let keys: (number | string)[] = [];
10
-
11
- let variables : { [key: number]: Syntax[] } = {};
12
- let selectors : { [key: number]: string } = {};
13
- export let fragments: string[] = [];
8
+ export let keys: number[] = [];
14
9
 
10
+ let variables : { [key: number]: { [key: number]: Syntax[] }} = {};
11
+ let selectors : { [key: number]: { [key: number]: string }} = {};
15
12
  export function start(): void {
13
+ reset();
14
+ }
15
+
16
+ export function trigger(input: string): void {
16
17
  try {
17
- let e = config.extract;
18
- if (!e) { return; }
19
- for (let i = 0; i < e.length; i+=3) {
20
- let source = e[i] as ExtractSource;
21
- let key = e[i+1] as number;
18
+ var parts = input && input.length > 0 ? input.split(/ (.*)/) : [Constant.Empty];
19
+ var key = parseInt(parts[0]);
20
+ var values = parts.length > 1 ? JSON.parse(parts[1]) : {};
21
+ variables[key] = {};
22
+ selectors[key] = {};
23
+ for (var v in values) {
24
+ let id = parseInt(v);
25
+ let value = values[v] as string;
26
+ let source = value.startsWith(Constant.Tilde) ? ExtractSource.Javascript : ExtractSource.Text;
22
27
  switch (source) {
23
28
  case ExtractSource.Javascript:
24
- let variable = e[i+2] as string;
25
- variables[key] = parse(variable);
26
- break;
27
- case ExtractSource.Cookie:
28
- /*Todo: Add cookie extract logic*/
29
+ let variable = value.substring(1, value.length);
30
+ variables[key][id] = parse(variable);
29
31
  break;
30
32
  case ExtractSource.Text:
31
- let match = e[i+2] as string;
32
- selectors[key] = match;
33
- break;
34
- case ExtractSource.Fragment:
35
- fragments = e[i+2] as string[];
33
+ selectors[key][id] = value;
36
34
  break;
37
35
  }
38
36
  }
@@ -49,13 +47,25 @@ export function clone(v: Syntax[]): Syntax[] {
49
47
  export function compute(): void {
50
48
  try {
51
49
  for (let v in variables) {
52
- let value = str(evaluate(clone(variables[v])));
53
- if (value) { update(v, value); }
54
- }
50
+ let key = parseInt(v);
51
+ if (!(key in keys)) {
52
+ let variableData = variables[key];
53
+ for (let v in variableData) {
54
+ let variableKey = parseInt(v);
55
+ let value = str(evaluate(clone(variableData[variableKey])));
56
+ if (value) { update(key, variableKey, value); }
57
+ }
55
58
 
56
- for (let s in selectors) {
57
- let node = document.querySelector(selectors[s] as string) as HTMLElement;
58
- if (node) { update(s, node.innerText); }
59
+ let selectorData = selectors[key];
60
+ for (let s in selectorData) {
61
+ let selectorKey = parseInt(s);
62
+ let nodes = document.querySelectorAll(selectorData[selectorKey]) as NodeListOf<HTMLElement>;
63
+ if (nodes) {
64
+ let text = Array.from(nodes).map(e => e.innerText)
65
+ update(key, selectorKey, text.join(Constant.Seperator).substring(0, Setting.ExtractLimit));
66
+ }
67
+ }
68
+ }
59
69
  }
60
70
  }
61
71
  catch (e) { internal.log(Code.Selector, Severity.Warning, e ? e.name : null); }
@@ -64,21 +74,22 @@ export function compute(): void {
64
74
  }
65
75
 
66
76
  export function reset(): void {
77
+ data = {};
67
78
  keys = [];
79
+ variables = {};
80
+ selectors = {};
68
81
  }
69
82
 
70
- export function update(key: string, value: string | number, force: boolean = false): void {
71
- if (!(key in data) || (key in data && data[key] !== value) || force ) {
72
- data[key] = value;
83
+ export function update(key: number, subkey: number, value: string): void {
84
+ if (!(key in data)) {
85
+ data[key] = []
73
86
  keys.push(key);
74
87
  }
88
+ data[key].push([subkey, value]);
75
89
  }
76
90
 
77
91
  export function stop(): void {
78
- data = {};
79
- keys = [];
80
- variables = {};
81
- selectors = {};
92
+ reset();
82
93
  }
83
94
 
84
95
  function parse(variable: string): Syntax[] {
@@ -15,6 +15,8 @@ import * as metric from "@src/data/metric";
15
15
  import * as ping from "@src/data/ping";
16
16
  import * as timeline from "@src/interaction/timeline";
17
17
  import * as region from "@src/layout/region";
18
+ import * as extract from "@src/data/extract";
19
+ import { report } from "@src/core/report";
18
20
 
19
21
  let discoverBytes: number = 0;
20
22
  let playbackBytes: number = 0;
@@ -168,7 +170,9 @@ function send(payload: string, zipped: Uint8Array, sequence: number, beacon: boo
168
170
  // Not all browsers support compression API and the support for it in supported browsers is still experimental
169
171
  if (sequence in transit) { transit[sequence].attempts++; } else { transit[sequence] = { data: payload, attempts: 1 }; }
170
172
  let xhr = new XMLHttpRequest();
171
- xhr.open("POST", url);
173
+ xhr.open("POST", url, true);
174
+ xhr.timeout = Setting.UploadTimeout;
175
+ xhr.ontimeout = () => { report(new Error(`${Constant.Timeout} : ${url}`)) };
172
176
  if (sequence !== null) { xhr.onreadystatechange = (): void => { measure(check)(xhr, sequence); }; }
173
177
  xhr.withCredentials = true;
174
178
  if (zipped) {
@@ -242,19 +246,26 @@ function delay(): number {
242
246
  }
243
247
 
244
248
  function response(payload: string): void {
245
- let parts = payload && payload.length > 0 ? payload.split(" ") : [Constant.Empty];
246
- switch (parts[0]) {
247
- case Constant.End:
248
- // Clear out session storage and end the session so we can start fresh the next time
249
- limit.trigger(Check.Server);
250
- break;
251
- case Constant.Upgrade:
252
- // Upgrade current session to send back playback information
253
- clarity.upgrade(Constant.Auto);
254
- break;
255
- case Constant.Action:
256
- // Invoke action callback, if configured and has a valid value
257
- if (config.action && parts.length > 1) { config.action(parts[1]); }
258
- break;
249
+ let lines = payload && payload.length > 0 ? payload.split("\n") : [];
250
+ for (var line of lines)
251
+ {
252
+ let parts = line && line.length > 0 ? line.split(/ (.*)/) : [Constant.Empty];
253
+ switch (parts[0]) {
254
+ case Constant.End:
255
+ // Clear out session storage and end the session so we can start fresh the next time
256
+ limit.trigger(Check.Server);
257
+ break;
258
+ case Constant.Upgrade:
259
+ // Upgrade current session to send back playback information
260
+ clarity.upgrade(Constant.Auto);
261
+ break;
262
+ case Constant.Action:
263
+ // Invoke action callback, if configured and has a valid value
264
+ if (config.action && parts.length > 1) { config.action(parts[1]); }
265
+ break;
266
+ case Constant.Extract:
267
+ if (parts.length > 1) { extract.trigger(parts[1]); }
268
+ break;
269
+ }
259
270
  }
260
271
  }
package/src/layout/dom.ts CHANGED
@@ -6,8 +6,6 @@ import hash from "@src/core/hash";
6
6
  import * as internal from "@src/diagnostic/internal";
7
7
  import * as region from "@src/layout/region";
8
8
  import * as selector from "@src/layout/selector";
9
- import * as mutation from "@src/layout/mutation";
10
- import * as extract from "@src/data/extract";
11
9
  let index: number = 1;
12
10
  let nodes: Node[] = [];
13
11
  let values: NodeValue[] = [];
@@ -15,7 +13,6 @@ let updateMap: number[] = [];
15
13
  let hashMap: { [hash: string]: number } = {};
16
14
  let override = [];
17
15
  let unmask = [];
18
- let updatedFragments: { [fragment: number]: string } = {};
19
16
  let maskText = [];
20
17
  let maskExclude = [];
21
18
  let maskDisable = [];
@@ -92,14 +89,12 @@ export function add(node: Node, parent: Node, data: NodeInfo, source: Source): v
92
89
  let previousId = getPreviousId(node);
93
90
  let parentValue: NodeValue = null;
94
91
  let regionId = region.exists(node) ? id : null;
95
- let fragmentId = null;
96
92
  let fraudId = fraudMap.has(node) ? fraudMap.get(node) : null;
97
93
  let privacyId = config.content ? Privacy.Sensitive : Privacy.TextImage
98
94
  if (parentId >= 0 && values[parentId]) {
99
95
  parentValue = values[parentId];
100
96
  parentValue.children.push(id);
101
97
  regionId = regionId === null ? parentValue.region : regionId;
102
- fragmentId = parentValue.fragment;
103
98
  fraudId = fraudId === null ? parentValue.metadata.fraud : fraudId;
104
99
  privacyId = parentValue.metadata.privacy;
105
100
  }
@@ -121,13 +116,12 @@ export function add(node: Node, parent: Node, data: NodeInfo, source: Source): v
121
116
  hash: null,
122
117
  region: regionId,
123
118
  metadata: { active: true, suspend: false, privacy: privacyId, position: null, fraud: fraudId, size: null },
124
- fragment: fragmentId,
125
119
  };
126
120
 
127
121
  privacy(node, values[id], parentValue);
128
122
  updateSelector(values[id]);
129
123
  size(values[id]);
130
- track(id, source, values[id].fragment);
124
+ track(id, source);
131
125
  }
132
126
 
133
127
  export function update(node: Node, parent: Node, data: NodeInfo, source: Source): void {
@@ -181,14 +175,9 @@ export function update(node: Node, parent: Node, data: NodeInfo, source: Source)
181
175
  }
182
176
  }
183
177
 
184
- // track node if it is a part of scheduled fragment mutation
185
- if(value.fragment && updatedFragments[value.fragment]) {
186
- changed = true;
187
- }
188
-
189
178
  // Update selector
190
179
  updateSelector(value);
191
- track(id, source, values[id].fragment, changed, parentChanged);
180
+ track(id, source, changed, parentChanged);
192
181
  }
193
182
  }
194
183
 
@@ -299,10 +288,6 @@ function updateSelector(value: NodeValue): void {
299
288
  value.selector = [selector.get(s, Selector.Alpha), selector.get(s, Selector.Beta)];
300
289
  value.hash = value.selector.map(x => x ? hash(x) : null) as [string, string];
301
290
  value.hash.forEach(h => hashMap[h] = value.id);
302
- // Match fragment configuration against both alpha and beta hash
303
- if (value.hash.some(h => extract.fragments.indexOf(h) !== -1)) {
304
- value.fragment = value.id;
305
- }
306
291
  }
307
292
 
308
293
  export function hashText(hash: string): string {
@@ -344,11 +329,7 @@ export function updates(): NodeValue[] {
344
329
  if (id in values) { output.push(values[id]); }
345
330
  }
346
331
  updateMap = [];
347
- for (let id in updatedFragments) {
348
- extract.update(updatedFragments[id], id, true)
349
- }
350
-
351
- updatedFragments = {}
332
+
352
333
  return output;
353
334
  }
354
335
 
@@ -377,19 +358,7 @@ function getPreviousId(node: Node): number {
377
358
  return id;
378
359
  }
379
360
 
380
- function track(id: number, source: Source, fragment: number = null, changed: boolean = true, parentChanged: boolean = false): void {
381
- // if updated node is a part of fragment and the fragment is not being tracked currently, schedule a mutation on the fragment node
382
- if (fragment && !updatedFragments[fragment]) {
383
- let node = getNode(fragment)
384
- let value = getValue(fragment);
385
- if (node && value) {
386
- mutation.schedule(node, true);
387
- value.hash.forEach(h => {
388
- if(extract.fragments.indexOf(h) !== -1) { updatedFragments[fragment] = h;}
389
- });
390
- }
391
- }
392
-
361
+ function track(id: number, source: Source, changed: boolean = true, parentChanged: boolean = false): void {
393
362
  // Keep track of the order in which mutations happened, they may not be sequential
394
363
  // Edge case: If an element is added later on, and pre-discovered element is moved as a child.
395
364
  // In that case, we need to reorder the pre-discovered element in the update list to keep visualization consistent.
@@ -210,7 +210,7 @@ async function processNodeList(list: NodeList, source: Source, timer: Timer): Pr
210
210
  }
211
211
  }
212
212
 
213
- export function schedule(node: Node, fragment: boolean = false): Node {
213
+ export function schedule(node: Node): Node {
214
214
  // Only schedule manual trigger for this node if it's not already in the queue
215
215
  if (queue.indexOf(node) < 0) { queue.push(node); }
216
216
 
@@ -218,19 +218,19 @@ export function schedule(node: Node, fragment: boolean = false): Node {
218
218
  // It's common for a webpage to call multiple synchronous "insertRule" / "deleteRule" calls.
219
219
  // And in those cases we do not wish to monitor changes multiple times for the same node.
220
220
  if (timeout) { clearTimeout(timeout); }
221
- timeout = setTimeout(() => { trigger(fragment) }, Setting.LookAhead);
221
+ timeout = setTimeout(() => { trigger() }, Setting.LookAhead);
222
222
 
223
223
  return node;
224
224
  }
225
225
 
226
- function trigger(fragment: boolean): void {
226
+ function trigger(): void {
227
227
  for (let node of queue) {
228
228
  // Generate a mutation for this node only if it still exists
229
229
  if (node) {
230
230
  let shadowRoot = node.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
231
231
  // Skip re-processing shadowRoot if it was already discovered
232
232
  if (shadowRoot && dom.has(node)) { continue; }
233
- generate(node, shadowRoot || fragment ? Constant.ChildList : Constant.CharacterData);
233
+ generate(node, shadowRoot ? Constant.ChildList : Constant.CharacterData);
234
234
  }
235
235
  }
236
236
  queue = [];
@@ -128,7 +128,7 @@ export default function (node: Node, source: Source): Node {
128
128
  case "HEAD":
129
129
  let head = { tag, attributes };
130
130
  let l = insideFrame && node.ownerDocument?.location ? node.ownerDocument.location : location;
131
- head.attributes[Constant.Base] = l.protocol + "//" + l.hostname + l.pathname;
131
+ head.attributes[Constant.Base] = l.protocol + "//" + l.host + l.pathname;
132
132
  dom[call](node, parent, head, source);
133
133
  break;
134
134
  case "BASE":
@@ -138,7 +138,7 @@ export default function (node: Node, source: Source): Node {
138
138
  // We create "a" element so we can generate protocol and hostname for relative paths like "/path/"
139
139
  let a = document.createElement("a");
140
140
  a.href = attributes["href"];
141
- baseHead.data.attributes[Constant.Base] = a.protocol + "//" + a.hostname + a.pathname;
141
+ baseHead.data.attributes[Constant.Base] = a.protocol + "//" + a.host + a.pathname;
142
142
  }
143
143
  break;
144
144
  case "STYLE":
@@ -91,5 +91,5 @@ export function stop(): void {
91
91
  function host(url: string): string {
92
92
  let a = document.createElement("a");
93
93
  a.href = url;
94
- return a.hostname;
94
+ return a.host;
95
95
  }
package/types/core.d.ts CHANGED
@@ -5,7 +5,6 @@ type TaskResolve = () => void;
5
5
  type UploadCallback = (data: string) => void;
6
6
  type Region = [number /* RegionId */, string /* Query Selector */];
7
7
  type Checksum = [number /* FraudId */, string /* Query Selector */];
8
- 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
 
@@ -127,7 +126,6 @@ export interface Config {
127
126
  mask?: string[];
128
127
  unmask?: string[];
129
128
  regions?: Region[];
130
- extract?: Extract[];
131
129
  cookies?: string[];
132
130
  fraud?: boolean;
133
131
  checksum?: Checksum[];
package/types/data.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Time } from "@clarity-types/core";
2
2
  export type Target = (number | Node);
3
- export type Token = (string | number | number[] | string[]);
3
+ export type Token = (string | number | number[] | string[] | (string | number)[]);
4
4
  export type DecodedToken = (any | any[]);
5
5
 
6
6
  export type MetadataCallback = (data: Metadata, playback: boolean) => void;
@@ -217,7 +217,8 @@ export const enum Setting {
217
217
  MinUploadDelay = 100, // Minimum time before we are ready to flush events to the server
218
218
  MaxUploadDelay = 30 * Time.Second, // Do flush out payload once every 30s,
219
219
  ExtractLimit = 10000, // Do not extract more than 10000 characters
220
- ChecksumPrecision = 24 // n-bit integer to represent token hash
220
+ ChecksumPrecision = 24, // n-bit integer to represent token hash
221
+ UploadTimeout = 15000 // Timeout in ms for XHR requests
221
222
  }
222
223
 
223
224
  export const enum Character {
@@ -263,6 +264,7 @@ export const enum Constant {
263
264
  End = "END",
264
265
  Upgrade = "UPGRADE",
265
266
  Action = "ACTION",
267
+ Extract = "EXTRACT",
266
268
  UserId = "userId",
267
269
  SessionId = "sessionId",
268
270
  PageId = "pageId",
@@ -284,7 +286,9 @@ export const enum Constant {
284
286
  Tilde = "~",
285
287
  ArrayStart = "[",
286
288
  ConditionStart = "{",
287
- ConditionEnd = "}"
289
+ ConditionEnd = "}",
290
+ Seperator = "<SEP>",
291
+ Timeout = "Timeout"
288
292
  }
289
293
 
290
294
  export const enum XMLReadyState {
@@ -404,7 +408,7 @@ export interface UpgradeData {
404
408
  }
405
409
 
406
410
  export interface ExtractData {
407
- [key: string]: string | number;
411
+ [key: number]: [number, string][]; // Array of [id, value] for every extracted data
408
412
  }
409
413
 
410
414
  export interface UploadData {
package/types/layout.d.ts CHANGED
@@ -168,7 +168,6 @@ export interface NodeValue {
168
168
  hash: [string, string];
169
169
  region: number;
170
170
  metadata: NodeMeta;
171
- fragment: number;
172
171
  }
173
172
 
174
173
  export interface NodeMeta {