clarity-js 0.8.2 → 0.8.4-beta

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 (102) 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 +5563 -5538
  5. package/build/clarity.min.js +1 -1
  6. package/build/clarity.module.js +5563 -5538
  7. package/build/clarity.performance.js +1 -1
  8. package/package.json +70 -70
  9. package/rollup.config.ts +98 -98
  10. package/src/clarity.ts +60 -60
  11. package/src/core/api.ts +8 -8
  12. package/src/core/config.ts +32 -32
  13. package/src/core/copy.ts +3 -3
  14. package/src/core/event.ts +53 -53
  15. package/src/core/hash.ts +19 -19
  16. package/src/core/history.ts +73 -73
  17. package/src/core/index.ts +83 -83
  18. package/src/core/measure.ts +19 -19
  19. package/src/core/report.ts +28 -28
  20. package/src/core/scrub.ts +196 -196
  21. package/src/core/task.ts +180 -180
  22. package/src/core/time.ts +19 -19
  23. package/src/core/timeout.ts +10 -10
  24. package/src/core/version.ts +2 -2
  25. package/src/data/baseline.ts +155 -155
  26. package/src/data/compress.ts +31 -31
  27. package/src/data/consent.ts +20 -20
  28. package/src/data/custom.ts +23 -23
  29. package/src/data/dimension.ts +53 -53
  30. package/src/data/encode.ts +140 -140
  31. package/src/data/envelope.ts +53 -53
  32. package/src/data/extract.ts +211 -211
  33. package/src/data/index.ts +46 -46
  34. package/src/data/limit.ts +44 -44
  35. package/src/data/metadata.ts +356 -356
  36. package/src/data/metric.ts +51 -51
  37. package/src/data/ping.ts +36 -36
  38. package/src/data/signal.ts +30 -30
  39. package/src/data/summary.ts +34 -34
  40. package/src/data/token.ts +39 -39
  41. package/src/data/upgrade.ts +37 -37
  42. package/src/data/upload.ts +283 -283
  43. package/src/data/variable.ts +83 -83
  44. package/src/diagnostic/encode.ts +40 -40
  45. package/src/diagnostic/fraud.ts +36 -36
  46. package/src/diagnostic/index.ts +15 -15
  47. package/src/diagnostic/internal.ts +28 -28
  48. package/src/diagnostic/script.ts +37 -37
  49. package/src/global.ts +6 -6
  50. package/src/index.ts +9 -9
  51. package/src/insight/blank.ts +14 -14
  52. package/src/insight/encode.ts +60 -60
  53. package/src/insight/snapshot.ts +114 -114
  54. package/src/interaction/change.ts +40 -40
  55. package/src/interaction/click.ts +194 -162
  56. package/src/interaction/clipboard.ts +34 -34
  57. package/src/interaction/encode.ts +196 -193
  58. package/src/interaction/index.ts +59 -59
  59. package/src/interaction/input.ts +59 -59
  60. package/src/interaction/pointer.ts +139 -139
  61. package/src/interaction/resize.ts +47 -47
  62. package/src/interaction/scroll.ts +124 -124
  63. package/src/interaction/selection.ts +68 -68
  64. package/src/interaction/submit.ts +32 -32
  65. package/src/interaction/timeline.ts +65 -65
  66. package/src/interaction/unload.ts +28 -28
  67. package/src/interaction/visibility.ts +26 -26
  68. package/src/layout/animation.ts +133 -133
  69. package/src/layout/discover.ts +31 -31
  70. package/src/layout/document.ts +48 -48
  71. package/src/layout/dom.ts +437 -437
  72. package/src/layout/encode.ts +147 -147
  73. package/src/layout/index.ts +41 -41
  74. package/src/layout/mutation.ts +409 -409
  75. package/src/layout/node.ts +292 -292
  76. package/src/layout/offset.ts +19 -19
  77. package/src/layout/region.ts +153 -153
  78. package/src/layout/schema.ts +63 -63
  79. package/src/layout/selector.ts +82 -82
  80. package/src/layout/style.ts +150 -150
  81. package/src/layout/target.ts +32 -32
  82. package/src/layout/traverse.ts +27 -27
  83. package/src/performance/blank.ts +7 -7
  84. package/src/performance/encode.ts +31 -31
  85. package/src/performance/index.ts +14 -14
  86. package/src/performance/interaction.ts +125 -125
  87. package/src/performance/navigation.ts +31 -31
  88. package/src/performance/observer.ts +108 -108
  89. package/src/queue.ts +33 -33
  90. package/test/core.test.ts +139 -139
  91. package/test/helper.ts +162 -162
  92. package/test/html/core.html +27 -27
  93. package/test/tsconfig.test.json +5 -5
  94. package/tsconfig.json +21 -21
  95. package/tslint.json +32 -32
  96. package/types/core.d.ts +153 -153
  97. package/types/data.d.ts +513 -510
  98. package/types/diagnostic.d.ts +24 -24
  99. package/types/index.d.ts +39 -39
  100. package/types/interaction.d.ts +168 -165
  101. package/types/layout.d.ts +272 -272
  102. package/types/performance.d.ts +64 -64
@@ -1,153 +1,153 @@
1
- import { Event, Setting } from "@clarity-types/data";
2
- import { InteractionState, RegionData, RegionState, RegionQueue, RegionVisibility } from "@clarity-types/layout";
3
- import { FunctionNames } from "@clarity-types/performance";
4
- import { time } from "@src/core/time";
5
- import * as dom from "@src/layout/dom";
6
- import encode from "@src/layout/encode";
7
-
8
- export let state: RegionState[] = [];
9
- let regionMap: WeakMap<Node, string> = null; // Maps region nodes => region name
10
- let regions: { [key: number]: RegionData } = {};
11
- let queue: RegionQueue[] = [];
12
- let watch = false;
13
- let observer: IntersectionObserver = null;
14
-
15
- export function start(): void {
16
- reset();
17
- observer = null;
18
- regionMap = new WeakMap();
19
- regions = {};
20
- queue = [];
21
- watch = window["IntersectionObserver"] ? true : false;
22
-
23
- }
24
-
25
- export function observe(node: Node, name: string): void {
26
- if (regionMap.has(node) === false) {
27
- regionMap.set(node, name);
28
- observer = observer === null && watch ? new IntersectionObserver(handler, {
29
- // Get notified as intersection continues to change
30
- // This allows us to process regions that get partially hidden during the lifetime of the page
31
- // See: https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#creating_an_intersection_observer
32
- // By default, intersection observers only fire an event when even a single pixel is visible and not thereafter.
33
- threshold: [0, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]
34
- }) : observer;
35
- if (observer && node && node.nodeType === Node.ELEMENT_NODE) {
36
- observer.observe(node as Element);
37
- }
38
- }
39
- }
40
-
41
- export function exists(node: Node): boolean {
42
- // Check if regionMap is not null before looking up a node
43
- // Since, dom module stops after region module, it's possible that we may set regionMap to be null
44
- // and still attempt to call exists on a late coming DOM mutation (or addition), effectively causing a script error
45
- return regionMap && regionMap.has(node);
46
- }
47
-
48
- export function track(id: number, event: Event): void {
49
- let node = dom.getNode(id);
50
- let data = id in regions ? regions[id] : { id, visibility: RegionVisibility.Rendered, interaction: InteractionState.None, name: regionMap.get(node) };
51
-
52
- // Determine the interaction state based on incoming event
53
- let interaction = InteractionState.None;
54
- switch (event) {
55
- case Event.Click: interaction = InteractionState.Clicked; break;
56
- case Event.Input: interaction = InteractionState.Input; break;
57
- }
58
- // Process updates to this region, if applicable
59
- process(node, data, interaction, data.visibility);
60
- }
61
-
62
- export function compute(): void {
63
- compute.dn = FunctionNames.RegionCompute;
64
- // Process any regions where we couldn't resolve an "id" for at the time of last intersection observer event
65
- // This could happen in cases where elements are not yet processed by Clarity's virtual DOM but browser reports a change, regardless.
66
- // For those cases we add them to the queue and re-process them below
67
- let q = [];
68
- for (let r of queue) {
69
- let id = dom.getId(r.node);
70
- if (id) {
71
- r.state.data.id = id;
72
- regions[id] = r.state.data;
73
- state.push(r.state);
74
- } else { q.push(r); }
75
- }
76
- queue = q;
77
-
78
- // Schedule encode only when we have at least one valid data entry
79
- if (state.length > 0) { encode(Event.Region); }
80
- }
81
-
82
- function handler(entries: IntersectionObserverEntry[]): void {
83
- for (let entry of entries) {
84
- let target = entry.target;
85
- let rect = entry.boundingClientRect;
86
- let overlap = entry.intersectionRect;
87
- let viewport = entry.rootBounds;
88
- // Only capture regions that have non-zero width or height to avoid tracking and sending regions
89
- // that cannot ever be seen by the user. In some cases, websites will have a multiple copy of the same region
90
- // like search box - one for desktop, and another for mobile. In those cases, CSS media queries determine which one should be visible.
91
- // 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
92
- if (regionMap.has(target) && rect.width + rect.height > 0 && viewport.width > 0 && viewport.height > 0) {
93
- let id = target ? dom.getId(target) : null;
94
- let data = id in regions ? regions[id] : { id, name: regionMap.get(target), interaction: InteractionState.None, visibility: RegionVisibility.Rendered };
95
-
96
- // For regions that have relatively smaller area, we look at intersection ratio and see the overlap relative to element's area
97
- // However, for larger regions, area of regions could be bigger than viewport and therefore comparison is relative to visible area
98
- let viewportRatio = overlap ? (overlap.width * overlap.height * 1.0) / (viewport.width * viewport.height) : 0;
99
- let visible = viewportRatio > Setting.ViewportIntersectionRatio || entry.intersectionRatio > Setting.IntersectionRatio;
100
- // If an element is either visible or was visible and has been scrolled to the end
101
- // 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.
102
- // 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
103
- let scrolledToEnd = (visible || data.visibility == RegionVisibility.Visible) && Math.abs(rect.top) + viewport.height > rect.height;
104
- // Process updates to this region, if applicable
105
- process(target, data, data.interaction,
106
- (scrolledToEnd ?
107
- RegionVisibility.ScrolledToEnd :
108
- (visible ? RegionVisibility.Visible : RegionVisibility.Rendered)));
109
-
110
- // Stop observing this element now that we have already received scrolled signal
111
- if (data.visibility >= RegionVisibility.ScrolledToEnd && observer) { observer.unobserve(target); }
112
- }
113
- }
114
- if (state.length > 0) { encode(Event.Region); }
115
- }
116
-
117
- function process(n: Node, d: RegionData, s: InteractionState, v: RegionVisibility): void {
118
- // Check if received a state that supersedes existing state
119
- let updated = s > d.interaction || v > d.visibility;
120
- d.interaction = s > d.interaction ? s : d.interaction;
121
- d.visibility = v > d.visibility ? v : d.visibility;
122
- // If the corresponding node is already discovered, update the internal state
123
- // Otherwise, track it in a queue to reprocess later.
124
- if (d.id) {
125
- if ((d.id in regions && updated) || !(d.id in regions)) {
126
- regions[d.id] = d;
127
- state.push(clone(d));
128
- }
129
- } else {
130
- // Get the time before adding to queue to ensure accurate event time
131
- queue.push({node: n, state: clone(d)});
132
- }
133
- }
134
-
135
- function clone(r: RegionData): RegionState {
136
- return { time: time(), data: { id: r.id, interaction: r.interaction, visibility: r.visibility, name: r.name }};
137
- }
138
-
139
- export function reset(): void {
140
- state = [];
141
- }
142
-
143
- export function stop(): void {
144
- reset();
145
- regionMap = null;
146
- regions = {};
147
- queue = [];
148
- if (observer) {
149
- observer.disconnect();
150
- observer = null;
151
- }
152
- watch = false;
153
- }
1
+ import { Event, Setting } from "@clarity-types/data";
2
+ import { InteractionState, RegionData, RegionState, RegionQueue, RegionVisibility } from "@clarity-types/layout";
3
+ import { FunctionNames } from "@clarity-types/performance";
4
+ import { time } from "@src/core/time";
5
+ import * as dom from "@src/layout/dom";
6
+ import encode from "@src/layout/encode";
7
+
8
+ export let state: RegionState[] = [];
9
+ let regionMap: WeakMap<Node, string> = null; // Maps region nodes => region name
10
+ let regions: { [key: number]: RegionData } = {};
11
+ let queue: RegionQueue[] = [];
12
+ let watch = false;
13
+ let observer: IntersectionObserver = null;
14
+
15
+ export function start(): void {
16
+ reset();
17
+ observer = null;
18
+ regionMap = new WeakMap();
19
+ regions = {};
20
+ queue = [];
21
+ watch = window["IntersectionObserver"] ? true : false;
22
+
23
+ }
24
+
25
+ export function observe(node: Node, name: string): void {
26
+ if (regionMap.has(node) === false) {
27
+ regionMap.set(node, name);
28
+ observer = observer === null && watch ? new IntersectionObserver(handler, {
29
+ // Get notified as intersection continues to change
30
+ // This allows us to process regions that get partially hidden during the lifetime of the page
31
+ // See: https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#creating_an_intersection_observer
32
+ // By default, intersection observers only fire an event when even a single pixel is visible and not thereafter.
33
+ threshold: [0, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]
34
+ }) : observer;
35
+ if (observer && node && node.nodeType === Node.ELEMENT_NODE) {
36
+ observer.observe(node as Element);
37
+ }
38
+ }
39
+ }
40
+
41
+ export function exists(node: Node): boolean {
42
+ // Check if regionMap is not null before looking up a node
43
+ // Since, dom module stops after region module, it's possible that we may set regionMap to be null
44
+ // and still attempt to call exists on a late coming DOM mutation (or addition), effectively causing a script error
45
+ return regionMap && regionMap.has(node);
46
+ }
47
+
48
+ export function track(id: number, event: Event): void {
49
+ let node = dom.getNode(id);
50
+ let data = id in regions ? regions[id] : { id, visibility: RegionVisibility.Rendered, interaction: InteractionState.None, name: regionMap.get(node) };
51
+
52
+ // Determine the interaction state based on incoming event
53
+ let interaction = InteractionState.None;
54
+ switch (event) {
55
+ case Event.Click: interaction = InteractionState.Clicked; break;
56
+ case Event.Input: interaction = InteractionState.Input; break;
57
+ }
58
+ // Process updates to this region, if applicable
59
+ process(node, data, interaction, data.visibility);
60
+ }
61
+
62
+ export function compute(): void {
63
+ compute.dn = FunctionNames.RegionCompute;
64
+ // Process any regions where we couldn't resolve an "id" for at the time of last intersection observer event
65
+ // This could happen in cases where elements are not yet processed by Clarity's virtual DOM but browser reports a change, regardless.
66
+ // For those cases we add them to the queue and re-process them below
67
+ let q = [];
68
+ for (let r of queue) {
69
+ let id = dom.getId(r.node);
70
+ if (id) {
71
+ r.state.data.id = id;
72
+ regions[id] = r.state.data;
73
+ state.push(r.state);
74
+ } else { q.push(r); }
75
+ }
76
+ queue = q;
77
+
78
+ // Schedule encode only when we have at least one valid data entry
79
+ if (state.length > 0) { encode(Event.Region); }
80
+ }
81
+
82
+ function handler(entries: IntersectionObserverEntry[]): void {
83
+ for (let entry of entries) {
84
+ let target = entry.target;
85
+ let rect = entry.boundingClientRect;
86
+ let overlap = entry.intersectionRect;
87
+ let viewport = entry.rootBounds;
88
+ // Only capture regions that have non-zero width or height to avoid tracking and sending regions
89
+ // that cannot ever be seen by the user. In some cases, websites will have a multiple copy of the same region
90
+ // like search box - one for desktop, and another for mobile. In those cases, CSS media queries determine which one should be visible.
91
+ // 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
92
+ if (regionMap.has(target) && rect.width + rect.height > 0 && viewport.width > 0 && viewport.height > 0) {
93
+ let id = target ? dom.getId(target) : null;
94
+ let data = id in regions ? regions[id] : { id, name: regionMap.get(target), interaction: InteractionState.None, visibility: RegionVisibility.Rendered };
95
+
96
+ // For regions that have relatively smaller area, we look at intersection ratio and see the overlap relative to element's area
97
+ // However, for larger regions, area of regions could be bigger than viewport and therefore comparison is relative to visible area
98
+ let viewportRatio = overlap ? (overlap.width * overlap.height * 1.0) / (viewport.width * viewport.height) : 0;
99
+ let visible = viewportRatio > Setting.ViewportIntersectionRatio || entry.intersectionRatio > Setting.IntersectionRatio;
100
+ // If an element is either visible or was visible and has been scrolled to the end
101
+ // 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.
102
+ // 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
103
+ let scrolledToEnd = (visible || data.visibility == RegionVisibility.Visible) && Math.abs(rect.top) + viewport.height > rect.height;
104
+ // Process updates to this region, if applicable
105
+ process(target, data, data.interaction,
106
+ (scrolledToEnd ?
107
+ RegionVisibility.ScrolledToEnd :
108
+ (visible ? RegionVisibility.Visible : RegionVisibility.Rendered)));
109
+
110
+ // Stop observing this element now that we have already received scrolled signal
111
+ if (data.visibility >= RegionVisibility.ScrolledToEnd && observer) { observer.unobserve(target); }
112
+ }
113
+ }
114
+ if (state.length > 0) { encode(Event.Region); }
115
+ }
116
+
117
+ function process(n: Node, d: RegionData, s: InteractionState, v: RegionVisibility): void {
118
+ // Check if received a state that supersedes existing state
119
+ let updated = s > d.interaction || v > d.visibility;
120
+ d.interaction = s > d.interaction ? s : d.interaction;
121
+ d.visibility = v > d.visibility ? v : d.visibility;
122
+ // If the corresponding node is already discovered, update the internal state
123
+ // Otherwise, track it in a queue to reprocess later.
124
+ if (d.id) {
125
+ if ((d.id in regions && updated) || !(d.id in regions)) {
126
+ regions[d.id] = d;
127
+ state.push(clone(d));
128
+ }
129
+ } else {
130
+ // Get the time before adding to queue to ensure accurate event time
131
+ queue.push({node: n, state: clone(d)});
132
+ }
133
+ }
134
+
135
+ function clone(r: RegionData): RegionState {
136
+ return { time: time(), data: { id: r.id, interaction: r.interaction, visibility: r.visibility, name: r.name }};
137
+ }
138
+
139
+ export function reset(): void {
140
+ state = [];
141
+ }
142
+
143
+ export function stop(): void {
144
+ reset();
145
+ regionMap = null;
146
+ regions = {};
147
+ queue = [];
148
+ if (observer) {
149
+ observer.disconnect();
150
+ observer = null;
151
+ }
152
+ watch = false;
153
+ }
@@ -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
+ }