clarity-js 0.8.42 → 0.8.44

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 (116) hide show
  1. package/README.md +26 -26
  2. package/build/clarity.extended.js +1 -1
  3. package/build/clarity.insight.js +1 -1
  4. package/build/clarity.js +6101 -6071
  5. package/build/clarity.min.js +1 -1
  6. package/build/clarity.module.js +6101 -6071
  7. package/build/clarity.performance.js +1 -1
  8. package/package.json +70 -70
  9. package/rollup.config.ts +161 -161
  10. package/src/clarity.ts +65 -65
  11. package/src/core/api.ts +8 -8
  12. package/src/core/config.ts +29 -29
  13. package/src/core/copy.ts +3 -3
  14. package/src/core/dynamic.ts +13 -7
  15. package/src/core/event.ts +53 -53
  16. package/src/core/hash.ts +19 -19
  17. package/src/core/history.ts +71 -71
  18. package/src/core/index.ts +81 -81
  19. package/src/core/measure.ts +19 -19
  20. package/src/core/report.ts +28 -28
  21. package/src/core/scrub.ts +204 -202
  22. package/src/core/task.ts +181 -181
  23. package/src/core/throttle.ts +46 -46
  24. package/src/core/time.ts +26 -26
  25. package/src/core/timeout.ts +10 -10
  26. package/src/core/version.ts +2 -2
  27. package/src/data/baseline.ts +162 -162
  28. package/src/data/compress.ts +31 -31
  29. package/src/data/consent.ts +77 -77
  30. package/src/data/cookie.ts +90 -0
  31. package/src/data/custom.ts +23 -23
  32. package/src/data/dimension.ts +53 -53
  33. package/src/data/encode.ts +155 -155
  34. package/src/data/envelope.ts +53 -53
  35. package/src/data/extract.ts +211 -211
  36. package/src/data/index.ts +51 -50
  37. package/src/data/limit.ts +44 -44
  38. package/src/data/metadata.ts +321 -408
  39. package/src/data/metric.ts +51 -51
  40. package/src/data/ping.ts +36 -36
  41. package/src/data/signal.ts +30 -30
  42. package/src/data/summary.ts +34 -34
  43. package/src/data/token.ts +39 -39
  44. package/src/data/upgrade.ts +44 -44
  45. package/src/data/upload.ts +333 -333
  46. package/src/data/util.ts +18 -0
  47. package/src/data/variable.ts +83 -83
  48. package/src/diagnostic/encode.ts +40 -40
  49. package/src/diagnostic/fraud.ts +36 -36
  50. package/src/diagnostic/index.ts +13 -13
  51. package/src/diagnostic/internal.ts +28 -28
  52. package/src/diagnostic/script.ts +35 -35
  53. package/src/dynamic/agent/blank.ts +2 -2
  54. package/src/dynamic/agent/crisp.ts +40 -40
  55. package/src/dynamic/agent/encode.ts +25 -25
  56. package/src/dynamic/agent/index.ts +8 -8
  57. package/src/dynamic/agent/livechat.ts +58 -58
  58. package/src/dynamic/agent/tidio.ts +44 -44
  59. package/src/global.ts +6 -6
  60. package/src/index.ts +9 -9
  61. package/src/insight/blank.ts +14 -14
  62. package/src/insight/encode.ts +60 -60
  63. package/src/insight/snapshot.ts +114 -114
  64. package/src/interaction/change.ts +38 -38
  65. package/src/interaction/click.ts +173 -173
  66. package/src/interaction/clipboard.ts +32 -32
  67. package/src/interaction/encode.ts +210 -210
  68. package/src/interaction/index.ts +60 -60
  69. package/src/interaction/input.ts +57 -57
  70. package/src/interaction/pointer.ts +137 -137
  71. package/src/interaction/resize.ts +50 -50
  72. package/src/interaction/scroll.ts +129 -129
  73. package/src/interaction/selection.ts +66 -66
  74. package/src/interaction/submit.ts +30 -30
  75. package/src/interaction/timeline.ts +69 -69
  76. package/src/interaction/unload.ts +26 -26
  77. package/src/interaction/visibility.ts +27 -27
  78. package/src/layout/animation.ts +133 -133
  79. package/src/layout/custom.ts +42 -42
  80. package/src/layout/discover.ts +31 -31
  81. package/src/layout/document.ts +46 -46
  82. package/src/layout/dom.ts +439 -439
  83. package/src/layout/encode.ts +154 -154
  84. package/src/layout/index.ts +42 -42
  85. package/src/layout/mutation.ts +411 -411
  86. package/src/layout/node.ts +294 -294
  87. package/src/layout/offset.ts +19 -19
  88. package/src/layout/region.ts +151 -151
  89. package/src/layout/schema.ts +63 -63
  90. package/src/layout/selector.ts +82 -82
  91. package/src/layout/style.ts +159 -159
  92. package/src/layout/target.ts +32 -32
  93. package/src/layout/traverse.ts +27 -27
  94. package/src/performance/blank.ts +9 -9
  95. package/src/performance/encode.ts +31 -31
  96. package/src/performance/index.ts +12 -12
  97. package/src/performance/interaction.ts +125 -125
  98. package/src/performance/navigation.ts +31 -31
  99. package/src/performance/observer.ts +112 -112
  100. package/src/queue.ts +33 -33
  101. package/test/core.test.ts +139 -139
  102. package/test/helper.ts +162 -162
  103. package/test/html/core.html +27 -27
  104. package/test/stub.test.ts +7 -7
  105. package/test/tsconfig.test.json +5 -5
  106. package/tsconfig.json +21 -21
  107. package/tslint.json +32 -32
  108. package/types/agent.d.ts +39 -39
  109. package/types/core.d.ts +150 -150
  110. package/types/data.d.ts +572 -571
  111. package/types/diagnostic.d.ts +24 -24
  112. package/types/global.d.ts +30 -30
  113. package/types/index.d.ts +40 -40
  114. package/types/interaction.d.ts +177 -177
  115. package/types/layout.d.ts +276 -276
  116. package/types/performance.d.ts +31 -31
@@ -1,151 +1,151 @@
1
- import { Event, Setting } from "@clarity-types/data";
2
- import { InteractionState, RegionData, RegionState, RegionQueue, RegionVisibility } from "@clarity-types/layout";
3
- import { time } from "@src/core/time";
4
- import * as dom from "@src/layout/dom";
5
- import encode from "@src/layout/encode";
6
-
7
- export let state: RegionState[] = [];
8
- let regionMap: WeakMap<Node, string> = null; // Maps region nodes => region name
9
- let regions: { [key: number]: RegionData } = {};
10
- let queue: RegionQueue[] = [];
11
- let watch = false;
12
- let observer: IntersectionObserver = null;
13
-
14
- export function start(): void {
15
- reset();
16
- observer = null;
17
- regionMap = new WeakMap();
18
- regions = {};
19
- queue = [];
20
- watch = window["IntersectionObserver"] ? true : false;
21
-
22
- }
23
-
24
- export function observe(node: Node, name: string): void {
25
- if (regionMap.has(node) === false) {
26
- regionMap.set(node, name);
27
- observer = observer === null && watch ? new IntersectionObserver(handler, {
28
- // Get notified as intersection continues to change
29
- // This allows us to process regions that get partially hidden during the lifetime of the page
30
- // See: https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#creating_an_intersection_observer
31
- // By default, intersection observers only fire an event when even a single pixel is visible and not thereafter.
32
- threshold: [0, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]
33
- }) : observer;
34
- if (observer && node && node.nodeType === Node.ELEMENT_NODE) {
35
- observer.observe(node as Element);
36
- }
37
- }
38
- }
39
-
40
- export function exists(node: Node): boolean {
41
- // Check if regionMap is not null before looking up a node
42
- // Since, dom module stops after region module, it's possible that we may set regionMap to be null
43
- // and still attempt to call exists on a late coming DOM mutation (or addition), effectively causing a script error
44
- return regionMap && regionMap.has(node);
45
- }
46
-
47
- export function track(id: number, event: Event): void {
48
- let node = dom.getNode(id);
49
- let data = id in regions ? regions[id] : { id, visibility: RegionVisibility.Rendered, interaction: InteractionState.None, name: regionMap.get(node) };
50
-
51
- // Determine the interaction state based on incoming event
52
- let interaction = InteractionState.None;
53
- switch (event) {
54
- case Event.Click: interaction = InteractionState.Clicked; break;
55
- case Event.Input: interaction = InteractionState.Input; break;
56
- }
57
- // Process updates to this region, if applicable
58
- process(node, data, interaction, data.visibility);
59
- }
60
-
61
- export function compute(): void {
62
- // Process any regions where we couldn't resolve an "id" for at the time of last intersection observer event
63
- // This could happen in cases where elements are not yet processed by Clarity's virtual DOM but browser reports a change, regardless.
64
- // For those cases we add them to the queue and re-process them below
65
- let q = [];
66
- for (let r of queue) {
67
- let id = dom.getId(r.node);
68
- if (id) {
69
- r.state.data.id = id;
70
- regions[id] = r.state.data;
71
- state.push(r.state);
72
- } else { q.push(r); }
73
- }
74
- queue = q;
75
-
76
- // Schedule encode only when we have at least one valid data entry
77
- if (state.length > 0) { encode(Event.Region); }
78
- }
79
-
80
- function handler(entries: IntersectionObserverEntry[]): void {
81
- for (let entry of entries) {
82
- let target = entry.target;
83
- let rect = entry.boundingClientRect;
84
- let overlap = entry.intersectionRect;
85
- let viewport = entry.rootBounds;
86
- // Only capture regions that have non-zero width or height to avoid tracking and sending regions
87
- // that cannot ever be seen by the user. In some cases, websites will have a multiple copy of the same region
88
- // like search box - one for desktop, and another for mobile. In those cases, CSS media queries determine which one should be visible.
89
- // Also, if these regions ever become non-zero width or height (through AJAX, user action or orientation change) - we will automatically start monitoring them from that point onwards
90
- if (regionMap.has(target) && rect.width + rect.height > 0 && viewport && viewport.width > 0 && viewport.height > 0) {
91
- let id = target ? dom.getId(target) : null;
92
- let data = id in regions ? regions[id] : { id, name: regionMap.get(target), interaction: InteractionState.None, visibility: RegionVisibility.Rendered };
93
-
94
- // For regions that have relatively smaller area, we look at intersection ratio and see the overlap relative to element's area
95
- // However, for larger regions, area of regions could be bigger than viewport and therefore comparison is relative to visible area
96
- let viewportRatio = overlap ? (overlap.width * overlap.height * 1.0) / (viewport.width * viewport.height) : 0;
97
- let visible = viewportRatio > Setting.ViewportIntersectionRatio || entry.intersectionRatio > Setting.IntersectionRatio;
98
- // If an element is either visible or was visible and has been scrolled to the end
99
- // i.e. Scrolled to end is determined by if the starting position of the element + the window height is more than the total element height.
100
- // starting position is relative to the viewport - so Intersection observer returns a negative value for rect.top to indicate that the element top is above the viewport
101
- let scrolledToEnd = (visible || data.visibility == RegionVisibility.Visible) && Math.abs(rect.top) + viewport.height > rect.height;
102
- // Process updates to this region, if applicable
103
- process(target, data, data.interaction,
104
- (scrolledToEnd ?
105
- RegionVisibility.ScrolledToEnd :
106
- (visible ? RegionVisibility.Visible : RegionVisibility.Rendered)));
107
-
108
- // Stop observing this element now that we have already received scrolled signal
109
- if (data.visibility >= RegionVisibility.ScrolledToEnd && observer) { observer.unobserve(target); }
110
- }
111
- }
112
- if (state.length > 0) { encode(Event.Region); }
113
- }
114
-
115
- function process(n: Node, d: RegionData, s: InteractionState, v: RegionVisibility): void {
116
- // Check if received a state that supersedes existing state
117
- let updated = s > d.interaction || v > d.visibility;
118
- d.interaction = s > d.interaction ? s : d.interaction;
119
- d.visibility = v > d.visibility ? v : d.visibility;
120
- // If the corresponding node is already discovered, update the internal state
121
- // Otherwise, track it in a queue to reprocess later.
122
- if (d.id) {
123
- if ((d.id in regions && updated) || !(d.id in regions)) {
124
- regions[d.id] = d;
125
- state.push(clone(d));
126
- }
127
- } else {
128
- // Get the time before adding to queue to ensure accurate event time
129
- queue.push({node: n, state: clone(d)});
130
- }
131
- }
132
-
133
- function clone(r: RegionData): RegionState {
134
- return { time: time(), data: { id: r.id, interaction: r.interaction, visibility: r.visibility, name: r.name }};
135
- }
136
-
137
- export function reset(): void {
138
- state = [];
139
- }
140
-
141
- export function stop(): void {
142
- reset();
143
- regionMap = null;
144
- regions = {};
145
- queue = [];
146
- if (observer) {
147
- observer.disconnect();
148
- observer = null;
149
- }
150
- watch = false;
151
- }
1
+ import { Event, Setting } from "@clarity-types/data";
2
+ import { InteractionState, RegionData, RegionState, RegionQueue, RegionVisibility } from "@clarity-types/layout";
3
+ import { time } from "@src/core/time";
4
+ import * as dom from "@src/layout/dom";
5
+ import encode from "@src/layout/encode";
6
+
7
+ export let state: RegionState[] = [];
8
+ let regionMap: WeakMap<Node, string> = null; // Maps region nodes => region name
9
+ let regions: { [key: number]: RegionData } = {};
10
+ let queue: RegionQueue[] = [];
11
+ let watch = false;
12
+ let observer: IntersectionObserver = null;
13
+
14
+ export function start(): void {
15
+ reset();
16
+ observer = null;
17
+ regionMap = new WeakMap();
18
+ regions = {};
19
+ queue = [];
20
+ watch = window["IntersectionObserver"] ? true : false;
21
+
22
+ }
23
+
24
+ export function observe(node: Node, name: string): void {
25
+ if (regionMap.has(node) === false) {
26
+ regionMap.set(node, name);
27
+ observer = observer === null && watch ? new IntersectionObserver(handler, {
28
+ // Get notified as intersection continues to change
29
+ // This allows us to process regions that get partially hidden during the lifetime of the page
30
+ // See: https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#creating_an_intersection_observer
31
+ // By default, intersection observers only fire an event when even a single pixel is visible and not thereafter.
32
+ threshold: [0, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]
33
+ }) : observer;
34
+ if (observer && node && node.nodeType === Node.ELEMENT_NODE) {
35
+ observer.observe(node as Element);
36
+ }
37
+ }
38
+ }
39
+
40
+ export function exists(node: Node): boolean {
41
+ // Check if regionMap is not null before looking up a node
42
+ // Since, dom module stops after region module, it's possible that we may set regionMap to be null
43
+ // and still attempt to call exists on a late coming DOM mutation (or addition), effectively causing a script error
44
+ return regionMap && regionMap.has(node);
45
+ }
46
+
47
+ export function track(id: number, event: Event): void {
48
+ let node = dom.getNode(id);
49
+ let data = id in regions ? regions[id] : { id, visibility: RegionVisibility.Rendered, interaction: InteractionState.None, name: regionMap.get(node) };
50
+
51
+ // Determine the interaction state based on incoming event
52
+ let interaction = InteractionState.None;
53
+ switch (event) {
54
+ case Event.Click: interaction = InteractionState.Clicked; break;
55
+ case Event.Input: interaction = InteractionState.Input; break;
56
+ }
57
+ // Process updates to this region, if applicable
58
+ process(node, data, interaction, data.visibility);
59
+ }
60
+
61
+ export function compute(): void {
62
+ // Process any regions where we couldn't resolve an "id" for at the time of last intersection observer event
63
+ // This could happen in cases where elements are not yet processed by Clarity's virtual DOM but browser reports a change, regardless.
64
+ // For those cases we add them to the queue and re-process them below
65
+ let q = [];
66
+ for (let r of queue) {
67
+ let id = dom.getId(r.node);
68
+ if (id) {
69
+ r.state.data.id = id;
70
+ regions[id] = r.state.data;
71
+ state.push(r.state);
72
+ } else { q.push(r); }
73
+ }
74
+ queue = q;
75
+
76
+ // Schedule encode only when we have at least one valid data entry
77
+ if (state.length > 0) { encode(Event.Region); }
78
+ }
79
+
80
+ function handler(entries: IntersectionObserverEntry[]): void {
81
+ for (let entry of entries) {
82
+ let target = entry.target;
83
+ let rect = entry.boundingClientRect;
84
+ let overlap = entry.intersectionRect;
85
+ let viewport = entry.rootBounds;
86
+ // Only capture regions that have non-zero width or height to avoid tracking and sending regions
87
+ // that cannot ever be seen by the user. In some cases, websites will have a multiple copy of the same region
88
+ // like search box - one for desktop, and another for mobile. In those cases, CSS media queries determine which one should be visible.
89
+ // Also, if these regions ever become non-zero width or height (through AJAX, user action or orientation change) - we will automatically start monitoring them from that point onwards
90
+ if (regionMap.has(target) && rect.width + rect.height > 0 && viewport && viewport.width > 0 && viewport.height > 0) {
91
+ let id = target ? dom.getId(target) : null;
92
+ let data = id in regions ? regions[id] : { id, name: regionMap.get(target), interaction: InteractionState.None, visibility: RegionVisibility.Rendered };
93
+
94
+ // For regions that have relatively smaller area, we look at intersection ratio and see the overlap relative to element's area
95
+ // However, for larger regions, area of regions could be bigger than viewport and therefore comparison is relative to visible area
96
+ let viewportRatio = overlap ? (overlap.width * overlap.height * 1.0) / (viewport.width * viewport.height) : 0;
97
+ let visible = viewportRatio > Setting.ViewportIntersectionRatio || entry.intersectionRatio > Setting.IntersectionRatio;
98
+ // If an element is either visible or was visible and has been scrolled to the end
99
+ // i.e. Scrolled to end is determined by if the starting position of the element + the window height is more than the total element height.
100
+ // starting position is relative to the viewport - so Intersection observer returns a negative value for rect.top to indicate that the element top is above the viewport
101
+ let scrolledToEnd = (visible || data.visibility == RegionVisibility.Visible) && Math.abs(rect.top) + viewport.height > rect.height;
102
+ // Process updates to this region, if applicable
103
+ process(target, data, data.interaction,
104
+ (scrolledToEnd ?
105
+ RegionVisibility.ScrolledToEnd :
106
+ (visible ? RegionVisibility.Visible : RegionVisibility.Rendered)));
107
+
108
+ // Stop observing this element now that we have already received scrolled signal
109
+ if (data.visibility >= RegionVisibility.ScrolledToEnd && observer) { observer.unobserve(target); }
110
+ }
111
+ }
112
+ if (state.length > 0) { encode(Event.Region); }
113
+ }
114
+
115
+ function process(n: Node, d: RegionData, s: InteractionState, v: RegionVisibility): void {
116
+ // Check if received a state that supersedes existing state
117
+ let updated = s > d.interaction || v > d.visibility;
118
+ d.interaction = s > d.interaction ? s : d.interaction;
119
+ d.visibility = v > d.visibility ? v : d.visibility;
120
+ // If the corresponding node is already discovered, update the internal state
121
+ // Otherwise, track it in a queue to reprocess later.
122
+ if (d.id) {
123
+ if ((d.id in regions && updated) || !(d.id in regions)) {
124
+ regions[d.id] = d;
125
+ state.push(clone(d));
126
+ }
127
+ } else {
128
+ // Get the time before adding to queue to ensure accurate event time
129
+ queue.push({node: n, state: clone(d)});
130
+ }
131
+ }
132
+
133
+ function clone(r: RegionData): RegionState {
134
+ return { time: time(), data: { id: r.id, interaction: r.interaction, visibility: r.visibility, name: r.name }};
135
+ }
136
+
137
+ export function reset(): void {
138
+ state = [];
139
+ }
140
+
141
+ export function stop(): void {
142
+ reset();
143
+ regionMap = null;
144
+ regions = {};
145
+ queue = [];
146
+ if (observer) {
147
+ observer.disconnect();
148
+ observer = null;
149
+ }
150
+ watch = false;
151
+ }
@@ -1,63 +1,63 @@
1
- import { Dimension, Metric, Setting } from "@clarity-types/data";
2
- import { Constant, JsonLD } from "@clarity-types/layout";
3
- import * as dimension from "@src/data/dimension";
4
- import * as metric from "@src/data/metric";
5
-
6
- const digitsRegex = /[^0-9\.]/g;
7
-
8
- /* JSON+LD (Linked Data) Recursive Parser */
9
- export function ld(json: any): void {
10
- for (let key of Object.keys(json)) {
11
- let value = json[key];
12
- if (key === JsonLD.Type && typeof value === "string") {
13
- value = value.toLowerCase();
14
- /* Normalizations */
15
- value = value.indexOf(JsonLD.Article) >= 0 || value.indexOf(JsonLD.Posting) >= 0 ? JsonLD.Article : value;
16
- switch (value) {
17
- case JsonLD.Article:
18
- case JsonLD.Recipe:
19
- dimension.log(Dimension.SchemaType, json[key]);
20
- dimension.log(Dimension.AuthorName, json[JsonLD.Creator]);
21
- dimension.log(Dimension.Headline, json[JsonLD.Headline]);
22
- break;
23
- case JsonLD.Product:
24
- dimension.log(Dimension.SchemaType, json[key]);
25
- dimension.log(Dimension.ProductName, json[JsonLD.Name]);
26
- dimension.log(Dimension.ProductSku, json[JsonLD.Sku]);
27
- if (json[JsonLD.Brand]) { dimension.log(Dimension.ProductBrand, json[JsonLD.Brand][JsonLD.Name]); }
28
- break;
29
- case JsonLD.AggregateRating:
30
- if (json[JsonLD.RatingValue]) {
31
- metric.max(Metric.RatingValue, num(json[JsonLD.RatingValue], Setting.RatingScale));
32
- metric.max(Metric.BestRating, num(json[JsonLD.BestRating]));
33
- metric.max(Metric.WorstRating, num(json[JsonLD.WorstRating]));
34
- }
35
- metric.max(Metric.RatingCount, num(json[JsonLD.RatingCount]));
36
- metric.max(Metric.ReviewCount, num(json[JsonLD.ReviewCount]));
37
- break;
38
- case JsonLD.Offer:
39
- dimension.log(Dimension.ProductAvailability, json[JsonLD.Availability]);
40
- dimension.log(Dimension.ProductCondition, json[JsonLD.ItemCondition]);
41
- dimension.log(Dimension.ProductCurrency, json[JsonLD.PriceCurrency]);
42
- dimension.log(Dimension.ProductSku, json[JsonLD.Sku]);
43
- metric.max(Metric.ProductPrice, num(json[JsonLD.Price]));
44
- break;
45
- case JsonLD.Brand:
46
- dimension.log(Dimension.ProductBrand, json[JsonLD.Name]);
47
- break;
48
- }
49
- }
50
- // Continue parsing nested objects
51
- if (value !== null && typeof(value) === Constant.Object) { ld(value); }
52
- }
53
- }
54
-
55
- function num(input: string | number, scale: number = 1): number {
56
- if (input !== null) {
57
- switch (typeof input) {
58
- case Constant.Number: return Math.round((input as number) * scale);
59
- case Constant.String: return Math.round(parseFloat((input as string).replace(digitsRegex, Constant.Empty)) * scale);
60
- }
61
- }
62
- return null;
63
- }
1
+ import { Dimension, Metric, Setting } from "@clarity-types/data";
2
+ import { Constant, JsonLD } from "@clarity-types/layout";
3
+ import * as dimension from "@src/data/dimension";
4
+ import * as metric from "@src/data/metric";
5
+
6
+ const digitsRegex = /[^0-9\.]/g;
7
+
8
+ /* JSON+LD (Linked Data) Recursive Parser */
9
+ export function ld(json: any): void {
10
+ for (let key of Object.keys(json)) {
11
+ let value = json[key];
12
+ if (key === JsonLD.Type && typeof value === "string") {
13
+ value = value.toLowerCase();
14
+ /* Normalizations */
15
+ value = value.indexOf(JsonLD.Article) >= 0 || value.indexOf(JsonLD.Posting) >= 0 ? JsonLD.Article : value;
16
+ switch (value) {
17
+ case JsonLD.Article:
18
+ case JsonLD.Recipe:
19
+ dimension.log(Dimension.SchemaType, json[key]);
20
+ dimension.log(Dimension.AuthorName, json[JsonLD.Creator]);
21
+ dimension.log(Dimension.Headline, json[JsonLD.Headline]);
22
+ break;
23
+ case JsonLD.Product:
24
+ dimension.log(Dimension.SchemaType, json[key]);
25
+ dimension.log(Dimension.ProductName, json[JsonLD.Name]);
26
+ dimension.log(Dimension.ProductSku, json[JsonLD.Sku]);
27
+ if (json[JsonLD.Brand]) { dimension.log(Dimension.ProductBrand, json[JsonLD.Brand][JsonLD.Name]); }
28
+ break;
29
+ case JsonLD.AggregateRating:
30
+ if (json[JsonLD.RatingValue]) {
31
+ metric.max(Metric.RatingValue, num(json[JsonLD.RatingValue], Setting.RatingScale));
32
+ metric.max(Metric.BestRating, num(json[JsonLD.BestRating]));
33
+ metric.max(Metric.WorstRating, num(json[JsonLD.WorstRating]));
34
+ }
35
+ metric.max(Metric.RatingCount, num(json[JsonLD.RatingCount]));
36
+ metric.max(Metric.ReviewCount, num(json[JsonLD.ReviewCount]));
37
+ break;
38
+ case JsonLD.Offer:
39
+ dimension.log(Dimension.ProductAvailability, json[JsonLD.Availability]);
40
+ dimension.log(Dimension.ProductCondition, json[JsonLD.ItemCondition]);
41
+ dimension.log(Dimension.ProductCurrency, json[JsonLD.PriceCurrency]);
42
+ dimension.log(Dimension.ProductSku, json[JsonLD.Sku]);
43
+ metric.max(Metric.ProductPrice, num(json[JsonLD.Price]));
44
+ break;
45
+ case JsonLD.Brand:
46
+ dimension.log(Dimension.ProductBrand, json[JsonLD.Name]);
47
+ break;
48
+ }
49
+ }
50
+ // Continue parsing nested objects
51
+ if (value !== null && typeof(value) === Constant.Object) { ld(value); }
52
+ }
53
+ }
54
+
55
+ function num(input: string | number, scale: number = 1): number {
56
+ if (input !== null) {
57
+ switch (typeof input) {
58
+ case Constant.Number: return Math.round((input as number) * scale);
59
+ case Constant.String: return Math.round(parseFloat((input as string).replace(digitsRegex, Constant.Empty)) * scale);
60
+ }
61
+ }
62
+ return null;
63
+ }
@@ -1,82 +1,82 @@
1
- import { Character } from "../../types/data";
2
- import { Constant, Selector, SelectorInput } from "../../types/layout";
3
-
4
- const excludeClassNames = Constant.ExcludeClassNames.split(Constant.Comma);
5
- let selectorMap: { [selector: string]: number[] } = {};
6
-
7
- export function reset(): void {
8
- selectorMap = {};
9
- }
10
-
11
- export function get(input: SelectorInput, type: Selector): string {
12
- let a = input.attributes;
13
- let prefix = input.prefix ? input.prefix[type] : null;
14
- let suffix = type === Selector.Alpha ? `${Constant.Tilde}${input.position-1}` : `:nth-of-type(${input.position})`;
15
- switch (input.tag) {
16
- case "STYLE":
17
- case "TITLE":
18
- case "LINK":
19
- case "META":
20
- case Constant.TextTag:
21
- case Constant.DocumentTag:
22
- return Constant.Empty;
23
- case "HTML":
24
- return Constant.HTML;
25
- default:
26
- if (prefix === null) { return Constant.Empty; }
27
- prefix = `${prefix}${Constant.Separator}`;
28
- input.tag = input.tag.indexOf(Constant.SvgPrefix) === 0 ? input.tag.substr(Constant.SvgPrefix.length) : input.tag;
29
- let selector = `${prefix}${input.tag}${suffix}`;
30
- let id = Constant.Id in a && a[Constant.Id].length > 0 ? a[Constant.Id] : null;
31
- let classes = input.tag !== Constant.BodyTag && Constant.Class in a && a[Constant.Class].length > 0 ? a[Constant.Class].trim().split(/\s+/).filter(c => filter(c)).join(Constant.Period) : null;
32
- if (classes && classes.length > 0) {
33
- if (type === Selector.Alpha) {
34
- // In Alpha mode, update selector to use class names, with relative positioning within the parent id container.
35
- // If the node has valid class name(s) then drop relative positioning within the parent path to keep things simple.
36
- let key = `${getDomPath(prefix)}${input.tag}${Constant.Dot}${classes}`;
37
- if (!(key in selectorMap)) { selectorMap[key] = []; }
38
- if (selectorMap[key].indexOf(input.id) < 0) { selectorMap[key].push(input.id); }
39
- selector = `${key}${Constant.Tilde}${selectorMap[key].indexOf(input.id)}`;
40
- } else {
41
- // In Beta mode, we continue to look at query selectors in context of the full page
42
- selector = `${prefix}${input.tag}.${classes}${suffix}`
43
- }
44
- }
45
- // Update selector to use "id" field when available. There are two exceptions:
46
- // (1) if "id" appears to be an auto generated string token, e.g. guid or a random id containing digits
47
- // (2) if "id" appears inside a shadow DOM, in which case we continue to prefix up to shadow DOM to prevent conflicts
48
- selector = id && filter(id) ? `${getDomPrefix(prefix)}${Constant.Hash}${id}` : selector;
49
- return selector;
50
- }
51
- }
52
-
53
- function getDomPrefix(prefix: string): string {
54
- const shadowDomStart = prefix.lastIndexOf(Constant.ShadowDomTag);
55
- const iframeDomStart = prefix.lastIndexOf(`${Constant.IFramePrefix}${Constant.HTML}`);
56
- const domStart = Math.max(shadowDomStart, iframeDomStart);
57
-
58
- if (domStart < 0) { return Constant.Empty; }
59
-
60
- return prefix.substring(0, prefix.indexOf(Constant.Separator, domStart) + 1);
61
- }
62
-
63
- function getDomPath(input: string): string {
64
- let parts = input.split(Constant.Separator);
65
- for (let i = 0; i < parts.length; i++) {
66
- let tIndex = parts[i].indexOf(Constant.Tilde);
67
- let dIndex = parts[i].indexOf(Constant.Dot);
68
- parts[i] = parts[i].substring(0, dIndex > 0 ? dIndex : (tIndex > 0 ? tIndex : parts[i].length));
69
- }
70
- return parts.join(Constant.Separator);
71
- }
72
-
73
- // Check if the given input string has digits or excluded class names
74
- function filter(value: string): boolean {
75
- if (!value) { return false; } // Do not process empty strings
76
- if (excludeClassNames.some(x => value.toLowerCase().indexOf(x) >= 0)) { return false; }
77
- for (let i = 0; i < value.length; i++) {
78
- let c = value.charCodeAt(i);
79
- if (c >= Character.Zero && c <= Character.Nine) { return false };
80
- }
81
- return true;
82
- }
1
+ import { Character } from "../../types/data";
2
+ import { Constant, Selector, SelectorInput } from "../../types/layout";
3
+
4
+ const excludeClassNames = Constant.ExcludeClassNames.split(Constant.Comma);
5
+ let selectorMap: { [selector: string]: number[] } = {};
6
+
7
+ export function reset(): void {
8
+ selectorMap = {};
9
+ }
10
+
11
+ export function get(input: SelectorInput, type: Selector): string {
12
+ let a = input.attributes;
13
+ let prefix = input.prefix ? input.prefix[type] : null;
14
+ let suffix = type === Selector.Alpha ? `${Constant.Tilde}${input.position-1}` : `:nth-of-type(${input.position})`;
15
+ switch (input.tag) {
16
+ case "STYLE":
17
+ case "TITLE":
18
+ case "LINK":
19
+ case "META":
20
+ case Constant.TextTag:
21
+ case Constant.DocumentTag:
22
+ return Constant.Empty;
23
+ case "HTML":
24
+ return Constant.HTML;
25
+ default:
26
+ if (prefix === null) { return Constant.Empty; }
27
+ prefix = `${prefix}${Constant.Separator}`;
28
+ input.tag = input.tag.indexOf(Constant.SvgPrefix) === 0 ? input.tag.substr(Constant.SvgPrefix.length) : input.tag;
29
+ let selector = `${prefix}${input.tag}${suffix}`;
30
+ let id = Constant.Id in a && a[Constant.Id].length > 0 ? a[Constant.Id] : null;
31
+ let classes = input.tag !== Constant.BodyTag && Constant.Class in a && a[Constant.Class].length > 0 ? a[Constant.Class].trim().split(/\s+/).filter(c => filter(c)).join(Constant.Period) : null;
32
+ if (classes && classes.length > 0) {
33
+ if (type === Selector.Alpha) {
34
+ // In Alpha mode, update selector to use class names, with relative positioning within the parent id container.
35
+ // If the node has valid class name(s) then drop relative positioning within the parent path to keep things simple.
36
+ let key = `${getDomPath(prefix)}${input.tag}${Constant.Dot}${classes}`;
37
+ if (!(key in selectorMap)) { selectorMap[key] = []; }
38
+ if (selectorMap[key].indexOf(input.id) < 0) { selectorMap[key].push(input.id); }
39
+ selector = `${key}${Constant.Tilde}${selectorMap[key].indexOf(input.id)}`;
40
+ } else {
41
+ // In Beta mode, we continue to look at query selectors in context of the full page
42
+ selector = `${prefix}${input.tag}.${classes}${suffix}`
43
+ }
44
+ }
45
+ // Update selector to use "id" field when available. There are two exceptions:
46
+ // (1) if "id" appears to be an auto generated string token, e.g. guid or a random id containing digits
47
+ // (2) if "id" appears inside a shadow DOM, in which case we continue to prefix up to shadow DOM to prevent conflicts
48
+ selector = id && filter(id) ? `${getDomPrefix(prefix)}${Constant.Hash}${id}` : selector;
49
+ return selector;
50
+ }
51
+ }
52
+
53
+ function getDomPrefix(prefix: string): string {
54
+ const shadowDomStart = prefix.lastIndexOf(Constant.ShadowDomTag);
55
+ const iframeDomStart = prefix.lastIndexOf(`${Constant.IFramePrefix}${Constant.HTML}`);
56
+ const domStart = Math.max(shadowDomStart, iframeDomStart);
57
+
58
+ if (domStart < 0) { return Constant.Empty; }
59
+
60
+ return prefix.substring(0, prefix.indexOf(Constant.Separator, domStart) + 1);
61
+ }
62
+
63
+ function getDomPath(input: string): string {
64
+ let parts = input.split(Constant.Separator);
65
+ for (let i = 0; i < parts.length; i++) {
66
+ let tIndex = parts[i].indexOf(Constant.Tilde);
67
+ let dIndex = parts[i].indexOf(Constant.Dot);
68
+ parts[i] = parts[i].substring(0, dIndex > 0 ? dIndex : (tIndex > 0 ? tIndex : parts[i].length));
69
+ }
70
+ return parts.join(Constant.Separator);
71
+ }
72
+
73
+ // Check if the given input string has digits or excluded class names
74
+ function filter(value: string): boolean {
75
+ if (!value) { return false; } // Do not process empty strings
76
+ if (excludeClassNames.some(x => value.toLowerCase().indexOf(x) >= 0)) { return false; }
77
+ for (let i = 0; i < value.length; i++) {
78
+ let c = value.charCodeAt(i);
79
+ if (c >= Character.Zero && c <= Character.Nine) { return false };
80
+ }
81
+ return true;
82
+ }