forked from snowplow/snowplow-javascript-tracker
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathelementsState.ts
More file actions
163 lines (152 loc) · 4.98 KB
/
Copy pathelementsState.ts
File metadata and controls
163 lines (152 loc) · 4.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
import type { Configuration } from './configuration';
import { Entities, type ElementStatisticsEntity } from './schemata';
export enum ElementStatus {
INITIAL,
CREATED,
DESTROYED,
EXPOSED,
PENDING,
OBSCURED,
}
/**
* Keeps track of per-element stuff we need to keep track of.
*/
type ElementState = {
/**
* Last known state for this element. Used to decide if events should be triggered or not when it's otherwise ambiguous.
*/
state: ElementStatus;
/**
* When the element was first seen/deemed "created"; for use in aggregate stats in future version.
*/
createdTs: number;
/**
* Last time we evaluated this element for a state change. Used for a delta to calculate cumulative visible time.
*/
lastObservationTs: number;
/**
* The above mentioned cumulative visible time.
*/
elapsedVisibleMs: number;
/**
* The pageview ID when we first observed this element.
*/
originalPageViewId: string;
/**
* The last position we saw of this element amongst the other matches we saw for this element.
*/
lastPosition: number;
/**
* The last non-0 size information available for this element.
*/
lastKnownSize?: DOMRect;
/**
* The other matches for this element's selector we last saw, of which the element is/was at `lastPosition`-1 position.
*/
matches: Set<Configuration>;
/**
* Smallest size dimensions seen so far for this element.
*/
minSize?: [number, number];
/**
* Largest size dimensions seen so far for this element.
*/
maxSize?: [number, number];
/**
* Largest vertical depth seen so far for this element, and the height the element was at that time.
*/
maxDepth?: [number, number];
/**
* Number of times this element has entered viewport so far.
*/
views: number;
};
/**
* Bank of per-element state that needs to be stored.
*/
const elementsState =
typeof WeakMap !== 'undefined' ? new WeakMap<Element, ElementState>() : new Map<Element, ElementState>();
/**
* Obtain element state from `elementState`, creating with sane defaults if element is unknown.
* @param target Element to obtain state for.
* @param initial Initial state to include in the state if created.
* @returns State for the target element.
*/
export function getState(target: Element, initial: Partial<ElementState> = {}): ElementState {
if (elementsState.has(target)) {
return elementsState.get(target)!;
} else {
const nowTs = performance.now();
const state: ElementState = {
state: ElementStatus.INITIAL,
matches: new Set(),
originalPageViewId: '',
createdTs: nowTs + performance.timeOrigin,
lastPosition: -1,
lastObservationTs: nowTs,
elapsedVisibleMs: 0,
views: 0,
...initial,
};
elementsState.set(target, state);
return state;
}
}
const stateLabels: Record<ElementStatus, string> = {
[ElementStatus.CREATED]: 'exists',
[ElementStatus.DESTROYED]: 'destroyed',
[ElementStatus.EXPOSED]: 'on screen',
[ElementStatus.INITIAL]: 'unknown',
[ElementStatus.OBSCURED]: 'off screen',
[ElementStatus.PENDING]: 'awaiting time criteria',
};
const compareSize = (a: [number, number], b?: [number, number]) => {
if (b === undefined) return 0;
const asize = a[0] * a[1];
const bsize = b[0] * b[1];
return asize - bsize;
};
export function aggregateStats(
element_name: string,
target: Element,
index: number,
matches: number
): ElementStatisticsEntity {
const state = getState(target);
const stateLabel = stateLabels[state.state];
const rect = target.getBoundingClientRect();
const curSize: [number, number] = [rect.width, rect.height];
const minSize = compareSize(curSize, state.minSize) <= 0 ? curSize : state.minSize!;
const maxSize = compareSize(curSize, state.maxSize) < 0 ? state.maxSize! : curSize;
const vpBottom = window.visualViewport
? window.visualViewport.height + window.visualViewport.pageTop
: window.innerHeight + window.scrollY;
const curDepth: [number, number] = [Math.min(vpBottom, rect.top + rect.height) - rect.top, rect.height];
const maxDepth =
state.maxDepth === undefined || state.maxDepth[1] === 0
? curDepth
: curDepth[1] === 0
? state.maxDepth
: curDepth[0] / curDepth[1] > state.maxDepth[0] / state.maxDepth[1]
? curDepth
: state.maxDepth;
Object.assign(state, { minSize, maxSize, maxDepth });
return {
schema: Entities.ELEMENT_STATISTICS,
data: {
element_name,
element_index: index,
element_matches: matches,
current_state: stateLabel,
min_size: minSize.join('x'),
current_size: curSize.join('x'),
max_size: maxSize.join('x'),
y_depth_ratio: curDepth[1] === 0 ? null : curDepth[0] / curDepth[1],
max_y_depth_ratio: maxDepth[1] === 0 ? null : maxDepth[0] / maxDepth[1],
max_y_depth: maxDepth.join('/'),
element_age_ms: Math.floor(performance.now() - (state.createdTs - performance.timeOrigin)),
times_in_view: state.views,
total_time_visible_ms: Math.floor(state.elapsedVisibleMs),
},
};
}