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.
- package/README.md +26 -26
- package/build/clarity.extended.js +1 -1
- package/build/clarity.insight.js +1 -1
- package/build/clarity.js +6101 -6071
- package/build/clarity.min.js +1 -1
- package/build/clarity.module.js +6101 -6071
- package/build/clarity.performance.js +1 -1
- package/package.json +70 -70
- package/rollup.config.ts +161 -161
- package/src/clarity.ts +65 -65
- package/src/core/api.ts +8 -8
- package/src/core/config.ts +29 -29
- package/src/core/copy.ts +3 -3
- package/src/core/dynamic.ts +13 -7
- package/src/core/event.ts +53 -53
- package/src/core/hash.ts +19 -19
- package/src/core/history.ts +71 -71
- package/src/core/index.ts +81 -81
- package/src/core/measure.ts +19 -19
- package/src/core/report.ts +28 -28
- package/src/core/scrub.ts +204 -202
- package/src/core/task.ts +181 -181
- package/src/core/throttle.ts +46 -46
- package/src/core/time.ts +26 -26
- package/src/core/timeout.ts +10 -10
- package/src/core/version.ts +2 -2
- package/src/data/baseline.ts +162 -162
- package/src/data/compress.ts +31 -31
- package/src/data/consent.ts +77 -77
- package/src/data/cookie.ts +90 -0
- package/src/data/custom.ts +23 -23
- package/src/data/dimension.ts +53 -53
- package/src/data/encode.ts +155 -155
- package/src/data/envelope.ts +53 -53
- package/src/data/extract.ts +211 -211
- package/src/data/index.ts +51 -50
- package/src/data/limit.ts +44 -44
- package/src/data/metadata.ts +321 -408
- package/src/data/metric.ts +51 -51
- package/src/data/ping.ts +36 -36
- package/src/data/signal.ts +30 -30
- package/src/data/summary.ts +34 -34
- package/src/data/token.ts +39 -39
- package/src/data/upgrade.ts +44 -44
- package/src/data/upload.ts +333 -333
- package/src/data/util.ts +18 -0
- package/src/data/variable.ts +83 -83
- package/src/diagnostic/encode.ts +40 -40
- package/src/diagnostic/fraud.ts +36 -36
- package/src/diagnostic/index.ts +13 -13
- package/src/diagnostic/internal.ts +28 -28
- package/src/diagnostic/script.ts +35 -35
- package/src/dynamic/agent/blank.ts +2 -2
- package/src/dynamic/agent/crisp.ts +40 -40
- package/src/dynamic/agent/encode.ts +25 -25
- package/src/dynamic/agent/index.ts +8 -8
- package/src/dynamic/agent/livechat.ts +58 -58
- package/src/dynamic/agent/tidio.ts +44 -44
- package/src/global.ts +6 -6
- package/src/index.ts +9 -9
- package/src/insight/blank.ts +14 -14
- package/src/insight/encode.ts +60 -60
- package/src/insight/snapshot.ts +114 -114
- package/src/interaction/change.ts +38 -38
- package/src/interaction/click.ts +173 -173
- package/src/interaction/clipboard.ts +32 -32
- package/src/interaction/encode.ts +210 -210
- package/src/interaction/index.ts +60 -60
- package/src/interaction/input.ts +57 -57
- package/src/interaction/pointer.ts +137 -137
- package/src/interaction/resize.ts +50 -50
- package/src/interaction/scroll.ts +129 -129
- package/src/interaction/selection.ts +66 -66
- package/src/interaction/submit.ts +30 -30
- package/src/interaction/timeline.ts +69 -69
- package/src/interaction/unload.ts +26 -26
- package/src/interaction/visibility.ts +27 -27
- package/src/layout/animation.ts +133 -133
- package/src/layout/custom.ts +42 -42
- package/src/layout/discover.ts +31 -31
- package/src/layout/document.ts +46 -46
- package/src/layout/dom.ts +439 -439
- package/src/layout/encode.ts +154 -154
- package/src/layout/index.ts +42 -42
- package/src/layout/mutation.ts +411 -411
- package/src/layout/node.ts +294 -294
- package/src/layout/offset.ts +19 -19
- package/src/layout/region.ts +151 -151
- package/src/layout/schema.ts +63 -63
- package/src/layout/selector.ts +82 -82
- package/src/layout/style.ts +159 -159
- package/src/layout/target.ts +32 -32
- package/src/layout/traverse.ts +27 -27
- package/src/performance/blank.ts +9 -9
- package/src/performance/encode.ts +31 -31
- package/src/performance/index.ts +12 -12
- package/src/performance/interaction.ts +125 -125
- package/src/performance/navigation.ts +31 -31
- package/src/performance/observer.ts +112 -112
- package/src/queue.ts +33 -33
- package/test/core.test.ts +139 -139
- package/test/helper.ts +162 -162
- package/test/html/core.html +27 -27
- package/test/stub.test.ts +7 -7
- package/test/tsconfig.test.json +5 -5
- package/tsconfig.json +21 -21
- package/tslint.json +32 -32
- package/types/agent.d.ts +39 -39
- package/types/core.d.ts +150 -150
- package/types/data.d.ts +572 -571
- package/types/diagnostic.d.ts +24 -24
- package/types/global.d.ts +30 -30
- package/types/index.d.ts +40 -40
- package/types/interaction.d.ts +177 -177
- package/types/layout.d.ts +276 -276
- package/types/performance.d.ts +31 -31
package/src/data/upload.ts
CHANGED
|
@@ -1,333 +1,333 @@
|
|
|
1
|
-
import { UploadCallback } from "@clarity-types/core";
|
|
2
|
-
import { BooleanFlag, Check, Code, Constant, EncodedPayload, Event, Metric, Setting, Severity, Token, Transit, UploadData, XMLReadyState } from "@clarity-types/data";
|
|
3
|
-
import * as clarity from "@src/clarity";
|
|
4
|
-
import config from "@src/core/config";
|
|
5
|
-
import measure from "@src/core/measure";
|
|
6
|
-
import { time } from "@src/core/time";
|
|
7
|
-
import { clearTimeout, setTimeout } from "@src/core/timeout";
|
|
8
|
-
import compress from "@src/data/compress";
|
|
9
|
-
import encode from "@src/data/encode";
|
|
10
|
-
import * as envelope from "@src/data/envelope";
|
|
11
|
-
import * as data from "@src/data/index";
|
|
12
|
-
import * as limit from "@src/data/limit";
|
|
13
|
-
import * as metadata from "@src/data/metadata";
|
|
14
|
-
import * as metric from "@src/data/metric";
|
|
15
|
-
import * as ping from "@src/data/ping";
|
|
16
|
-
import * as internal from "@src/diagnostic/internal";
|
|
17
|
-
import * as timeline from "@src/interaction/timeline";
|
|
18
|
-
import * as region from "@src/layout/region";
|
|
19
|
-
import * as extract from "@src/data/extract";
|
|
20
|
-
import * as style from "@src/layout/style";
|
|
21
|
-
import { report } from "@src/core/report";
|
|
22
|
-
import { signalsEvent } from "@src/data/signal";
|
|
23
|
-
import { snapshot } from "@src/insight/snapshot";
|
|
24
|
-
import * as dynamic from "@src/core/dynamic";
|
|
25
|
-
|
|
26
|
-
let discoverBytes: number = 0;
|
|
27
|
-
let playbackBytes: number = 0;
|
|
28
|
-
let playback: string[];
|
|
29
|
-
let analysis: string[];
|
|
30
|
-
let timeout: number = null;
|
|
31
|
-
let transit: Transit;
|
|
32
|
-
let active: boolean;
|
|
33
|
-
let queuedTime: number = 0;
|
|
34
|
-
let leanLimit = false;
|
|
35
|
-
export let track: UploadData;
|
|
36
|
-
|
|
37
|
-
export function start(): void {
|
|
38
|
-
active = true;
|
|
39
|
-
discoverBytes = 0;
|
|
40
|
-
playbackBytes = 0;
|
|
41
|
-
leanLimit = false;
|
|
42
|
-
queuedTime = 0;
|
|
43
|
-
playback = [];
|
|
44
|
-
analysis = [];
|
|
45
|
-
transit = {};
|
|
46
|
-
track = null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function queue(tokens: Token[], transmit: boolean = true): void {
|
|
50
|
-
if (!active) {
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
let now = time();
|
|
55
|
-
let type = tokens.length > 1 ? tokens[1] : null;
|
|
56
|
-
let event = JSON.stringify(tokens);
|
|
57
|
-
|
|
58
|
-
if (!config.lean) {
|
|
59
|
-
leanLimit = false;
|
|
60
|
-
} else if (!leanLimit && playbackBytes + event.length > Setting.PlaybackBytesLimit) {
|
|
61
|
-
internal.log(Code.LeanLimit, Severity.Info);
|
|
62
|
-
leanLimit = true;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
switch (type) {
|
|
66
|
-
case Event.Discover:
|
|
67
|
-
if (leanLimit) { break; }
|
|
68
|
-
discoverBytes += event.length;
|
|
69
|
-
case Event.Box:
|
|
70
|
-
case Event.Mutation:
|
|
71
|
-
case Event.Snapshot:
|
|
72
|
-
case Event.StyleSheetAdoption:
|
|
73
|
-
case Event.StyleSheetUpdate:
|
|
74
|
-
case Event.Animation:
|
|
75
|
-
case Event.CustomElement:
|
|
76
|
-
if (leanLimit) { break; }
|
|
77
|
-
playbackBytes += event.length;
|
|
78
|
-
playback.push(event);
|
|
79
|
-
break;
|
|
80
|
-
default:
|
|
81
|
-
analysis.push(event);
|
|
82
|
-
break;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Increment event count metric
|
|
86
|
-
metric.count(Metric.EventCount);
|
|
87
|
-
|
|
88
|
-
// Following two checks are precautionary and act as a fail safe mechanism to get out of unexpected situations.
|
|
89
|
-
// Check 1: If for any reason the upload hasn't happened after waiting for 2x the config.delay time,
|
|
90
|
-
// reset the timer. This allows Clarity to attempt an upload again.
|
|
91
|
-
let gap = delay();
|
|
92
|
-
if (now - queuedTime > (gap * 2)) {
|
|
93
|
-
clearTimeout(timeout);
|
|
94
|
-
timeout = null;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Transmit Check: When transmit is set to true (default), it indicates that we should schedule an upload
|
|
98
|
-
// However, in certain scenarios - like metric calculation - which are triggered as part of an existing upload
|
|
99
|
-
// We enrich the data going out with the existing upload. In these cases, call to upload comes with 'transmit' set to false.
|
|
100
|
-
if (transmit && timeout === null) {
|
|
101
|
-
if (type !== Event.Ping) { ping.reset(); }
|
|
102
|
-
timeout = setTimeout(upload, gap);
|
|
103
|
-
queuedTime = now;
|
|
104
|
-
limit.check(playbackBytes);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function stop(): void {
|
|
109
|
-
clearTimeout(timeout);
|
|
110
|
-
upload(true);
|
|
111
|
-
discoverBytes = 0;
|
|
112
|
-
playbackBytes = 0;
|
|
113
|
-
leanLimit = false;
|
|
114
|
-
queuedTime = 0;
|
|
115
|
-
playback = [];
|
|
116
|
-
analysis = [];
|
|
117
|
-
transit = {};
|
|
118
|
-
track = null;
|
|
119
|
-
active = false;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async function upload(final: boolean = false): Promise<void> {
|
|
123
|
-
if (!active) {
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
timeout = null;
|
|
128
|
-
|
|
129
|
-
// Check if we can send playback bytes over the wire or not
|
|
130
|
-
// For better instrumentation coverage, we send playback bytes from second sequence onwards
|
|
131
|
-
// And, we only send playback metric when we are able to send the playback bytes back to server
|
|
132
|
-
let sendPlaybackBytes = config.lean === false && playbackBytes > 0 && (playbackBytes < Setting.MaxFirstPayloadBytes || envelope.data.sequence > 0);
|
|
133
|
-
if (sendPlaybackBytes) { metric.max(Metric.Playback, BooleanFlag.True); }
|
|
134
|
-
|
|
135
|
-
// CAUTION: Ensure "transmit" is set to false in the queue function for following events
|
|
136
|
-
// Otherwise you run a risk of infinite loop.
|
|
137
|
-
region.compute();
|
|
138
|
-
timeline.compute();
|
|
139
|
-
data.compute();
|
|
140
|
-
style.compute();
|
|
141
|
-
|
|
142
|
-
// Treat this as the last payload only if final boolean was explicitly set to true.
|
|
143
|
-
// In real world tests, we noticed that certain third party scripts (e.g. https://www.npmjs.com/package/raven-js)
|
|
144
|
-
// could inject function arguments for internal tracking (likely stack traces for script errors).
|
|
145
|
-
// For these edge cases, we want to ensure that an injected object (e.g. {"key": "value"}) isn't mistaken to be true.
|
|
146
|
-
let last = final === true;
|
|
147
|
-
|
|
148
|
-
// In some cases envelope has null data because it's part of the shutdown process while there's one upload call queued which might introduce runtime error
|
|
149
|
-
if(!envelope.data) return;
|
|
150
|
-
|
|
151
|
-
let e = JSON.stringify(envelope.envelope(last));
|
|
152
|
-
let a = `[${analysis.join()}]`;
|
|
153
|
-
|
|
154
|
-
let p = sendPlaybackBytes ? `[${playback.join()}]` : Constant.Empty;
|
|
155
|
-
|
|
156
|
-
// For final (beacon) payloads, If size is too large, we need to remove playback data
|
|
157
|
-
if (last && p.length > 0 && (e.length + a.length + p.length > Setting.MaxBeaconPayloadBytes)) {
|
|
158
|
-
p = Constant.Empty;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
let encoded: EncodedPayload = {e, a, p};
|
|
162
|
-
|
|
163
|
-
// Get the payload ready for sending over the wire
|
|
164
|
-
// We also attempt to compress the payload if it is not the last payload and the browser supports it
|
|
165
|
-
// In all other cases, we continue to send back string value
|
|
166
|
-
let payload = stringify(encoded);
|
|
167
|
-
let zipped = last ? null : await compress(payload);
|
|
168
|
-
metric.sum(Metric.TotalBytes, zipped ? zipped.length : payload.length);
|
|
169
|
-
send(payload, zipped, envelope.data.sequence, last);
|
|
170
|
-
|
|
171
|
-
// Clear out events now that payload has been dispatched
|
|
172
|
-
analysis = [];
|
|
173
|
-
if (sendPlaybackBytes) {
|
|
174
|
-
playback = [];
|
|
175
|
-
playbackBytes = 0;
|
|
176
|
-
discoverBytes = 0;
|
|
177
|
-
leanLimit = false;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function stringify(encoded: EncodedPayload): string {
|
|
182
|
-
return encoded.p.length > 0 ? `{"e":${encoded.e},"a":${encoded.a},"p":${encoded.p}}` : `{"e":${encoded.e},"a":${encoded.a}}`;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function send(payload: string, zipped: Uint8Array, sequence: number, beacon: boolean = false): void {
|
|
186
|
-
// Upload data if a valid URL is defined in the config
|
|
187
|
-
if (typeof config.upload === Constant.String) {
|
|
188
|
-
const url = config.upload as string;
|
|
189
|
-
let dispatched = false;
|
|
190
|
-
|
|
191
|
-
// If it's the last payload, attempt to upload using sendBeacon first.
|
|
192
|
-
// The advantage to using sendBeacon is that browser can decide to upload asynchronously, improving chances of success
|
|
193
|
-
// However, we don't want to rely on it for every payload, since we have no ability to retry if the upload failed.
|
|
194
|
-
// Also, in case of sendBeacon, we do not have a way to alter HTTP headers and therefore can't send compressed payload
|
|
195
|
-
if (beacon && navigator && navigator["sendBeacon"]) {
|
|
196
|
-
try {
|
|
197
|
-
// Navigator needs to be bound to sendBeacon before it is used to avoid errors in some browsers
|
|
198
|
-
dispatched = navigator.sendBeacon.bind(navigator)(url, payload);
|
|
199
|
-
if (dispatched) {
|
|
200
|
-
done(sequence);
|
|
201
|
-
}
|
|
202
|
-
} catch(error) {
|
|
203
|
-
// If sendBeacon fails, we do nothing and continue with XHR upload
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Before initiating XHR upload, we check if the data has already been uploaded using sendBeacon
|
|
208
|
-
// There are two cases when dispatched could still be false:
|
|
209
|
-
// a) It's not the last payload, and therefore we didn't attempt sending sendBeacon
|
|
210
|
-
// b) It's the last payload, however, we failed to queue sendBeacon call and need to now fall back to XHR.
|
|
211
|
-
// E.g. if data is over 64KB, several user agents (like Chrome) will reject to queue the sendBeacon call.
|
|
212
|
-
if (dispatched === false) {
|
|
213
|
-
// While tracking payload for retry, we only track string value of the payload to err on the safe side
|
|
214
|
-
// Not all browsers support compression API and the support for it in supported browsers is still experimental
|
|
215
|
-
if (sequence in transit) { transit[sequence].attempts++; } else { transit[sequence] = { data: payload, attempts: 1 }; }
|
|
216
|
-
let xhr = new XMLHttpRequest();
|
|
217
|
-
xhr.open("POST", url, true);
|
|
218
|
-
xhr.timeout = Setting.UploadTimeout;
|
|
219
|
-
xhr.ontimeout = () => { report(new Error(`${Constant.Timeout} : ${url}`)) };
|
|
220
|
-
if (sequence !== null) { xhr.onreadystatechange = (): void => { measure(check)(xhr, sequence); }; }
|
|
221
|
-
xhr.withCredentials = true;
|
|
222
|
-
if (zipped) {
|
|
223
|
-
// If we do have valid compressed array, send it with appropriate HTTP headers so server can decode it appropriately
|
|
224
|
-
xhr.setRequestHeader(Constant.Accept, Constant.ClarityGzip);
|
|
225
|
-
xhr.send(zipped);
|
|
226
|
-
} else {
|
|
227
|
-
// In all other cases, continue sending string back to the server
|
|
228
|
-
xhr.send(payload);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
} else if (config.upload) {
|
|
232
|
-
const callback = config.upload as UploadCallback;
|
|
233
|
-
callback(payload);
|
|
234
|
-
done(sequence);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function check(xhr: XMLHttpRequest, sequence: number): void {
|
|
239
|
-
var transitData = transit[sequence];
|
|
240
|
-
if (xhr && xhr.readyState === XMLReadyState.Done && transitData) {
|
|
241
|
-
// Attempt send payload again (as configured in settings) if we do not receive a success (2XX) response code back from the server
|
|
242
|
-
if ((xhr.status < 200 || xhr.status > 208) && transitData.attempts <= Setting.RetryLimit) {
|
|
243
|
-
// We re-attempt in all cases except when server explicitly rejects our request with 4XX error
|
|
244
|
-
if (xhr.status >= 400 && xhr.status < 500) {
|
|
245
|
-
// In case of a 4XX response from the server, we bail out instead of trying again
|
|
246
|
-
limit.trigger(Check.Server);
|
|
247
|
-
} else {
|
|
248
|
-
// Browser will send status = 0 when it refuses to put network request over the wire
|
|
249
|
-
// This could happen for several reasons, couple of known ones are:
|
|
250
|
-
// 1: Browsers block upload because of content security policy violation
|
|
251
|
-
// 2: Safari will terminate pending XHR requests with status code 0 if the user navigates away from the page
|
|
252
|
-
// In any case, we switch the upload URL to fallback configuration (if available) before re-trying one more time
|
|
253
|
-
if (xhr.status === 0) { config.upload = config.fallback ? config.fallback : config.upload; }
|
|
254
|
-
// Capture the status code and number of attempts so we can report it back to the server
|
|
255
|
-
track = { sequence, attempts: transitData.attempts, status: xhr.status };
|
|
256
|
-
encode(Event.Upload);
|
|
257
|
-
// In all other cases, re-attempt sending the same data
|
|
258
|
-
// For retry we always fallback to string payload, even though we may have attempted
|
|
259
|
-
// sending zipped payload earlier
|
|
260
|
-
send(transitData.data, null, sequence);
|
|
261
|
-
}
|
|
262
|
-
} else {
|
|
263
|
-
track = { sequence, attempts: transitData.attempts, status: xhr.status };
|
|
264
|
-
// Send back an event only if we were not successful in our first attempt
|
|
265
|
-
if (transitData.attempts > 1) { encode(Event.Upload); }
|
|
266
|
-
// Handle response if it was a 200 response with a valid body
|
|
267
|
-
if (xhr.status === 200 && xhr.responseText) { response(xhr.responseText); }
|
|
268
|
-
// If we exhausted our retries then trigger Clarity's shutdown for this page since the data will be incomplete
|
|
269
|
-
if (xhr.status === 0) {
|
|
270
|
-
// And, right before we terminate the session, we will attempt one last time to see if we can use
|
|
271
|
-
// different transport option (sendBeacon vs. XHR) to get this data to the server for analysis purposes
|
|
272
|
-
send(transitData.data, null, sequence, true);
|
|
273
|
-
limit.trigger(Check.Retry);
|
|
274
|
-
}
|
|
275
|
-
// Signal that this request completed successfully
|
|
276
|
-
if (xhr.status >= 200 && xhr.status <= 208) { done(sequence); }
|
|
277
|
-
// Stop tracking this payload now that it's all done
|
|
278
|
-
delete transit[sequence];
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function done(sequence: number): void {
|
|
284
|
-
// If we everything went successfully, and it is the first sequence, save this session for future reference
|
|
285
|
-
if (sequence === 1) {
|
|
286
|
-
metadata.save();
|
|
287
|
-
metadata.callback();
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function delay(): number {
|
|
292
|
-
// Progressively increase delay as we continue to send more payloads from the client to the server
|
|
293
|
-
// If we are not uploading data to a server, and instead invoking UploadCallback, in that case keep returning configured value
|
|
294
|
-
let gap = config.lean === false && discoverBytes > 0 ? Setting.MinUploadDelay : envelope.data.sequence * config.delay;
|
|
295
|
-
return typeof config.upload === Constant.String ? Math.max(Math.min(gap, Setting.MaxUploadDelay), Setting.MinUploadDelay) : config.delay;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function response(payload: string): void {
|
|
299
|
-
let lines = payload && payload.length > 0 ? payload.split("\n") : [];
|
|
300
|
-
for (var line of lines)
|
|
301
|
-
{
|
|
302
|
-
let parts = line && line.length > 0 ? line.split(/ (.*)/) : [Constant.Empty];
|
|
303
|
-
switch (parts[0]) {
|
|
304
|
-
case Constant.End:
|
|
305
|
-
// Clear out session storage and end the session so we can start fresh the next time
|
|
306
|
-
limit.trigger(Check.Server);
|
|
307
|
-
break;
|
|
308
|
-
case Constant.Upgrade:
|
|
309
|
-
// Upgrade current session to send back playback information
|
|
310
|
-
clarity.upgrade(Constant.Auto);
|
|
311
|
-
break;
|
|
312
|
-
case Constant.Action:
|
|
313
|
-
// Invoke action callback, if configured and has a valid value
|
|
314
|
-
if (config.action && parts.length > 1) { config.action(parts[1]); }
|
|
315
|
-
break;
|
|
316
|
-
case Constant.Extract:
|
|
317
|
-
if (parts.length > 1) { extract.trigger(parts[1]); }
|
|
318
|
-
break;
|
|
319
|
-
case Constant.Signal:
|
|
320
|
-
if (parts.length > 1) { signalsEvent(parts[1]); }
|
|
321
|
-
break;
|
|
322
|
-
case Constant.Module:
|
|
323
|
-
if (parts.length > 1) {
|
|
324
|
-
dynamic.event(parts[1]);
|
|
325
|
-
}
|
|
326
|
-
break;
|
|
327
|
-
case Constant.Snapshot:
|
|
328
|
-
config.lean = false; // Disable lean mode to ensure we can send playback information to server.
|
|
329
|
-
snapshot();
|
|
330
|
-
break;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
}
|
|
1
|
+
import { UploadCallback } from "@clarity-types/core";
|
|
2
|
+
import { BooleanFlag, Check, Code, Constant, EncodedPayload, Event, Metric, Setting, Severity, Token, Transit, UploadData, XMLReadyState } from "@clarity-types/data";
|
|
3
|
+
import * as clarity from "@src/clarity";
|
|
4
|
+
import config from "@src/core/config";
|
|
5
|
+
import measure from "@src/core/measure";
|
|
6
|
+
import { time } from "@src/core/time";
|
|
7
|
+
import { clearTimeout, setTimeout } from "@src/core/timeout";
|
|
8
|
+
import compress from "@src/data/compress";
|
|
9
|
+
import encode from "@src/data/encode";
|
|
10
|
+
import * as envelope from "@src/data/envelope";
|
|
11
|
+
import * as data from "@src/data/index";
|
|
12
|
+
import * as limit from "@src/data/limit";
|
|
13
|
+
import * as metadata from "@src/data/metadata";
|
|
14
|
+
import * as metric from "@src/data/metric";
|
|
15
|
+
import * as ping from "@src/data/ping";
|
|
16
|
+
import * as internal from "@src/diagnostic/internal";
|
|
17
|
+
import * as timeline from "@src/interaction/timeline";
|
|
18
|
+
import * as region from "@src/layout/region";
|
|
19
|
+
import * as extract from "@src/data/extract";
|
|
20
|
+
import * as style from "@src/layout/style";
|
|
21
|
+
import { report } from "@src/core/report";
|
|
22
|
+
import { signalsEvent } from "@src/data/signal";
|
|
23
|
+
import { snapshot } from "@src/insight/snapshot";
|
|
24
|
+
import * as dynamic from "@src/core/dynamic";
|
|
25
|
+
|
|
26
|
+
let discoverBytes: number = 0;
|
|
27
|
+
let playbackBytes: number = 0;
|
|
28
|
+
let playback: string[];
|
|
29
|
+
let analysis: string[];
|
|
30
|
+
let timeout: number = null;
|
|
31
|
+
let transit: Transit;
|
|
32
|
+
let active: boolean;
|
|
33
|
+
let queuedTime: number = 0;
|
|
34
|
+
let leanLimit = false;
|
|
35
|
+
export let track: UploadData;
|
|
36
|
+
|
|
37
|
+
export function start(): void {
|
|
38
|
+
active = true;
|
|
39
|
+
discoverBytes = 0;
|
|
40
|
+
playbackBytes = 0;
|
|
41
|
+
leanLimit = false;
|
|
42
|
+
queuedTime = 0;
|
|
43
|
+
playback = [];
|
|
44
|
+
analysis = [];
|
|
45
|
+
transit = {};
|
|
46
|
+
track = null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function queue(tokens: Token[], transmit: boolean = true): void {
|
|
50
|
+
if (!active) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let now = time();
|
|
55
|
+
let type = tokens.length > 1 ? tokens[1] : null;
|
|
56
|
+
let event = JSON.stringify(tokens);
|
|
57
|
+
|
|
58
|
+
if (!config.lean) {
|
|
59
|
+
leanLimit = false;
|
|
60
|
+
} else if (!leanLimit && playbackBytes + event.length > Setting.PlaybackBytesLimit) {
|
|
61
|
+
internal.log(Code.LeanLimit, Severity.Info);
|
|
62
|
+
leanLimit = true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
switch (type) {
|
|
66
|
+
case Event.Discover:
|
|
67
|
+
if (leanLimit) { break; }
|
|
68
|
+
discoverBytes += event.length;
|
|
69
|
+
case Event.Box:
|
|
70
|
+
case Event.Mutation:
|
|
71
|
+
case Event.Snapshot:
|
|
72
|
+
case Event.StyleSheetAdoption:
|
|
73
|
+
case Event.StyleSheetUpdate:
|
|
74
|
+
case Event.Animation:
|
|
75
|
+
case Event.CustomElement:
|
|
76
|
+
if (leanLimit) { break; }
|
|
77
|
+
playbackBytes += event.length;
|
|
78
|
+
playback.push(event);
|
|
79
|
+
break;
|
|
80
|
+
default:
|
|
81
|
+
analysis.push(event);
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Increment event count metric
|
|
86
|
+
metric.count(Metric.EventCount);
|
|
87
|
+
|
|
88
|
+
// Following two checks are precautionary and act as a fail safe mechanism to get out of unexpected situations.
|
|
89
|
+
// Check 1: If for any reason the upload hasn't happened after waiting for 2x the config.delay time,
|
|
90
|
+
// reset the timer. This allows Clarity to attempt an upload again.
|
|
91
|
+
let gap = delay();
|
|
92
|
+
if (now - queuedTime > (gap * 2)) {
|
|
93
|
+
clearTimeout(timeout);
|
|
94
|
+
timeout = null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Transmit Check: When transmit is set to true (default), it indicates that we should schedule an upload
|
|
98
|
+
// However, in certain scenarios - like metric calculation - which are triggered as part of an existing upload
|
|
99
|
+
// We enrich the data going out with the existing upload. In these cases, call to upload comes with 'transmit' set to false.
|
|
100
|
+
if (transmit && timeout === null) {
|
|
101
|
+
if (type !== Event.Ping) { ping.reset(); }
|
|
102
|
+
timeout = setTimeout(upload, gap);
|
|
103
|
+
queuedTime = now;
|
|
104
|
+
limit.check(playbackBytes);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function stop(): void {
|
|
109
|
+
clearTimeout(timeout);
|
|
110
|
+
upload(true);
|
|
111
|
+
discoverBytes = 0;
|
|
112
|
+
playbackBytes = 0;
|
|
113
|
+
leanLimit = false;
|
|
114
|
+
queuedTime = 0;
|
|
115
|
+
playback = [];
|
|
116
|
+
analysis = [];
|
|
117
|
+
transit = {};
|
|
118
|
+
track = null;
|
|
119
|
+
active = false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function upload(final: boolean = false): Promise<void> {
|
|
123
|
+
if (!active) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
timeout = null;
|
|
128
|
+
|
|
129
|
+
// Check if we can send playback bytes over the wire or not
|
|
130
|
+
// For better instrumentation coverage, we send playback bytes from second sequence onwards
|
|
131
|
+
// And, we only send playback metric when we are able to send the playback bytes back to server
|
|
132
|
+
let sendPlaybackBytes = config.lean === false && playbackBytes > 0 && (playbackBytes < Setting.MaxFirstPayloadBytes || envelope.data.sequence > 0);
|
|
133
|
+
if (sendPlaybackBytes) { metric.max(Metric.Playback, BooleanFlag.True); }
|
|
134
|
+
|
|
135
|
+
// CAUTION: Ensure "transmit" is set to false in the queue function for following events
|
|
136
|
+
// Otherwise you run a risk of infinite loop.
|
|
137
|
+
region.compute();
|
|
138
|
+
timeline.compute();
|
|
139
|
+
data.compute();
|
|
140
|
+
style.compute();
|
|
141
|
+
|
|
142
|
+
// Treat this as the last payload only if final boolean was explicitly set to true.
|
|
143
|
+
// In real world tests, we noticed that certain third party scripts (e.g. https://www.npmjs.com/package/raven-js)
|
|
144
|
+
// could inject function arguments for internal tracking (likely stack traces for script errors).
|
|
145
|
+
// For these edge cases, we want to ensure that an injected object (e.g. {"key": "value"}) isn't mistaken to be true.
|
|
146
|
+
let last = final === true;
|
|
147
|
+
|
|
148
|
+
// In some cases envelope has null data because it's part of the shutdown process while there's one upload call queued which might introduce runtime error
|
|
149
|
+
if(!envelope.data) return;
|
|
150
|
+
|
|
151
|
+
let e = JSON.stringify(envelope.envelope(last));
|
|
152
|
+
let a = `[${analysis.join()}]`;
|
|
153
|
+
|
|
154
|
+
let p = sendPlaybackBytes ? `[${playback.join()}]` : Constant.Empty;
|
|
155
|
+
|
|
156
|
+
// For final (beacon) payloads, If size is too large, we need to remove playback data
|
|
157
|
+
if (last && p.length > 0 && (e.length + a.length + p.length > Setting.MaxBeaconPayloadBytes)) {
|
|
158
|
+
p = Constant.Empty;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let encoded: EncodedPayload = {e, a, p};
|
|
162
|
+
|
|
163
|
+
// Get the payload ready for sending over the wire
|
|
164
|
+
// We also attempt to compress the payload if it is not the last payload and the browser supports it
|
|
165
|
+
// In all other cases, we continue to send back string value
|
|
166
|
+
let payload = stringify(encoded);
|
|
167
|
+
let zipped = last ? null : await compress(payload);
|
|
168
|
+
metric.sum(Metric.TotalBytes, zipped ? zipped.length : payload.length);
|
|
169
|
+
send(payload, zipped, envelope.data.sequence, last);
|
|
170
|
+
|
|
171
|
+
// Clear out events now that payload has been dispatched
|
|
172
|
+
analysis = [];
|
|
173
|
+
if (sendPlaybackBytes) {
|
|
174
|
+
playback = [];
|
|
175
|
+
playbackBytes = 0;
|
|
176
|
+
discoverBytes = 0;
|
|
177
|
+
leanLimit = false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function stringify(encoded: EncodedPayload): string {
|
|
182
|
+
return encoded.p.length > 0 ? `{"e":${encoded.e},"a":${encoded.a},"p":${encoded.p}}` : `{"e":${encoded.e},"a":${encoded.a}}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function send(payload: string, zipped: Uint8Array, sequence: number, beacon: boolean = false): void {
|
|
186
|
+
// Upload data if a valid URL is defined in the config
|
|
187
|
+
if (typeof config.upload === Constant.String) {
|
|
188
|
+
const url = config.upload as string;
|
|
189
|
+
let dispatched = false;
|
|
190
|
+
|
|
191
|
+
// If it's the last payload, attempt to upload using sendBeacon first.
|
|
192
|
+
// The advantage to using sendBeacon is that browser can decide to upload asynchronously, improving chances of success
|
|
193
|
+
// However, we don't want to rely on it for every payload, since we have no ability to retry if the upload failed.
|
|
194
|
+
// Also, in case of sendBeacon, we do not have a way to alter HTTP headers and therefore can't send compressed payload
|
|
195
|
+
if (beacon && navigator && navigator["sendBeacon"]) {
|
|
196
|
+
try {
|
|
197
|
+
// Navigator needs to be bound to sendBeacon before it is used to avoid errors in some browsers
|
|
198
|
+
dispatched = navigator.sendBeacon.bind(navigator)(url, payload);
|
|
199
|
+
if (dispatched) {
|
|
200
|
+
done(sequence);
|
|
201
|
+
}
|
|
202
|
+
} catch(error) {
|
|
203
|
+
// If sendBeacon fails, we do nothing and continue with XHR upload
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Before initiating XHR upload, we check if the data has already been uploaded using sendBeacon
|
|
208
|
+
// There are two cases when dispatched could still be false:
|
|
209
|
+
// a) It's not the last payload, and therefore we didn't attempt sending sendBeacon
|
|
210
|
+
// b) It's the last payload, however, we failed to queue sendBeacon call and need to now fall back to XHR.
|
|
211
|
+
// E.g. if data is over 64KB, several user agents (like Chrome) will reject to queue the sendBeacon call.
|
|
212
|
+
if (dispatched === false) {
|
|
213
|
+
// While tracking payload for retry, we only track string value of the payload to err on the safe side
|
|
214
|
+
// Not all browsers support compression API and the support for it in supported browsers is still experimental
|
|
215
|
+
if (sequence in transit) { transit[sequence].attempts++; } else { transit[sequence] = { data: payload, attempts: 1 }; }
|
|
216
|
+
let xhr = new XMLHttpRequest();
|
|
217
|
+
xhr.open("POST", url, true);
|
|
218
|
+
xhr.timeout = Setting.UploadTimeout;
|
|
219
|
+
xhr.ontimeout = () => { report(new Error(`${Constant.Timeout} : ${url}`)) };
|
|
220
|
+
if (sequence !== null) { xhr.onreadystatechange = (): void => { measure(check)(xhr, sequence); }; }
|
|
221
|
+
xhr.withCredentials = true;
|
|
222
|
+
if (zipped) {
|
|
223
|
+
// If we do have valid compressed array, send it with appropriate HTTP headers so server can decode it appropriately
|
|
224
|
+
xhr.setRequestHeader(Constant.Accept, Constant.ClarityGzip);
|
|
225
|
+
xhr.send(zipped);
|
|
226
|
+
} else {
|
|
227
|
+
// In all other cases, continue sending string back to the server
|
|
228
|
+
xhr.send(payload);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
} else if (config.upload) {
|
|
232
|
+
const callback = config.upload as UploadCallback;
|
|
233
|
+
callback(payload);
|
|
234
|
+
done(sequence);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function check(xhr: XMLHttpRequest, sequence: number): void {
|
|
239
|
+
var transitData = transit[sequence];
|
|
240
|
+
if (xhr && xhr.readyState === XMLReadyState.Done && transitData) {
|
|
241
|
+
// Attempt send payload again (as configured in settings) if we do not receive a success (2XX) response code back from the server
|
|
242
|
+
if ((xhr.status < 200 || xhr.status > 208) && transitData.attempts <= Setting.RetryLimit) {
|
|
243
|
+
// We re-attempt in all cases except when server explicitly rejects our request with 4XX error
|
|
244
|
+
if (xhr.status >= 400 && xhr.status < 500) {
|
|
245
|
+
// In case of a 4XX response from the server, we bail out instead of trying again
|
|
246
|
+
limit.trigger(Check.Server);
|
|
247
|
+
} else {
|
|
248
|
+
// Browser will send status = 0 when it refuses to put network request over the wire
|
|
249
|
+
// This could happen for several reasons, couple of known ones are:
|
|
250
|
+
// 1: Browsers block upload because of content security policy violation
|
|
251
|
+
// 2: Safari will terminate pending XHR requests with status code 0 if the user navigates away from the page
|
|
252
|
+
// In any case, we switch the upload URL to fallback configuration (if available) before re-trying one more time
|
|
253
|
+
if (xhr.status === 0) { config.upload = config.fallback ? config.fallback : config.upload; }
|
|
254
|
+
// Capture the status code and number of attempts so we can report it back to the server
|
|
255
|
+
track = { sequence, attempts: transitData.attempts, status: xhr.status };
|
|
256
|
+
encode(Event.Upload);
|
|
257
|
+
// In all other cases, re-attempt sending the same data
|
|
258
|
+
// For retry we always fallback to string payload, even though we may have attempted
|
|
259
|
+
// sending zipped payload earlier
|
|
260
|
+
send(transitData.data, null, sequence);
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
track = { sequence, attempts: transitData.attempts, status: xhr.status };
|
|
264
|
+
// Send back an event only if we were not successful in our first attempt
|
|
265
|
+
if (transitData.attempts > 1) { encode(Event.Upload); }
|
|
266
|
+
// Handle response if it was a 200 response with a valid body
|
|
267
|
+
if (xhr.status === 200 && xhr.responseText) { response(xhr.responseText); }
|
|
268
|
+
// If we exhausted our retries then trigger Clarity's shutdown for this page since the data will be incomplete
|
|
269
|
+
if (xhr.status === 0) {
|
|
270
|
+
// And, right before we terminate the session, we will attempt one last time to see if we can use
|
|
271
|
+
// different transport option (sendBeacon vs. XHR) to get this data to the server for analysis purposes
|
|
272
|
+
send(transitData.data, null, sequence, true);
|
|
273
|
+
limit.trigger(Check.Retry);
|
|
274
|
+
}
|
|
275
|
+
// Signal that this request completed successfully
|
|
276
|
+
if (xhr.status >= 200 && xhr.status <= 208) { done(sequence); }
|
|
277
|
+
// Stop tracking this payload now that it's all done
|
|
278
|
+
delete transit[sequence];
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function done(sequence: number): void {
|
|
284
|
+
// If we everything went successfully, and it is the first sequence, save this session for future reference
|
|
285
|
+
if (sequence === 1) {
|
|
286
|
+
metadata.save();
|
|
287
|
+
metadata.callback();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function delay(): number {
|
|
292
|
+
// Progressively increase delay as we continue to send more payloads from the client to the server
|
|
293
|
+
// If we are not uploading data to a server, and instead invoking UploadCallback, in that case keep returning configured value
|
|
294
|
+
let gap = config.lean === false && discoverBytes > 0 ? Setting.MinUploadDelay : envelope.data.sequence * config.delay;
|
|
295
|
+
return typeof config.upload === Constant.String ? Math.max(Math.min(gap, Setting.MaxUploadDelay), Setting.MinUploadDelay) : config.delay;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function response(payload: string): void {
|
|
299
|
+
let lines = payload && payload.length > 0 ? payload.split("\n") : [];
|
|
300
|
+
for (var line of lines)
|
|
301
|
+
{
|
|
302
|
+
let parts = line && line.length > 0 ? line.split(/ (.*)/) : [Constant.Empty];
|
|
303
|
+
switch (parts[0]) {
|
|
304
|
+
case Constant.End:
|
|
305
|
+
// Clear out session storage and end the session so we can start fresh the next time
|
|
306
|
+
limit.trigger(Check.Server);
|
|
307
|
+
break;
|
|
308
|
+
case Constant.Upgrade:
|
|
309
|
+
// Upgrade current session to send back playback information
|
|
310
|
+
clarity.upgrade(Constant.Auto);
|
|
311
|
+
break;
|
|
312
|
+
case Constant.Action:
|
|
313
|
+
// Invoke action callback, if configured and has a valid value
|
|
314
|
+
if (config.action && parts.length > 1) { config.action(parts[1]); }
|
|
315
|
+
break;
|
|
316
|
+
case Constant.Extract:
|
|
317
|
+
if (parts.length > 1) { extract.trigger(parts[1]); }
|
|
318
|
+
break;
|
|
319
|
+
case Constant.Signal:
|
|
320
|
+
if (parts.length > 1) { signalsEvent(parts[1]); }
|
|
321
|
+
break;
|
|
322
|
+
case Constant.Module:
|
|
323
|
+
if (parts.length > 1) {
|
|
324
|
+
dynamic.event(parts[1]);
|
|
325
|
+
}
|
|
326
|
+
break;
|
|
327
|
+
case Constant.Snapshot:
|
|
328
|
+
config.lean = false; // Disable lean mode to ensure we can send playback information to server.
|
|
329
|
+
snapshot();
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|