@dataxamas/tracker-js 1.0.1
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 +256 -0
- package/dist/index.cjs +672 -0
- package/dist/index.d.cts +142 -0
- package/dist/index.d.ts +142 -0
- package/dist/index.js +672 -0
- package/dist/rrweb-F6UF6U3D.js +17611 -0
- package/dist/rrweb-M7O3D3Q3.cjs +17611 -0
- package/package.json +62 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/constants.ts
|
|
2
|
+
var BASE_URL = "https://dataxamas.izakdvlpr.com";
|
|
3
|
+
|
|
4
|
+
// src/utils.ts
|
|
5
|
+
var isBrowser = () => typeof window !== "undefined" && typeof document !== "undefined";
|
|
6
|
+
|
|
7
|
+
// src/exceptions.ts
|
|
8
|
+
function normalizeError(error) {
|
|
9
|
+
if (error instanceof Error) {
|
|
10
|
+
return { name: error.name || "Error", message: error.message || String(error), stack: error.stack };
|
|
11
|
+
}
|
|
12
|
+
if (typeof error === "string") {
|
|
13
|
+
return { name: "Error", message: error };
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
return { name: "Error", message: JSON.stringify(error) };
|
|
17
|
+
} catch (e2) {
|
|
18
|
+
return { name: "Error", message: String(error) };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
var DataxamasExceptions = class {
|
|
22
|
+
constructor(config) {
|
|
23
|
+
this.config = config;
|
|
24
|
+
this.installed = false;
|
|
25
|
+
}
|
|
26
|
+
captureException(error, context) {
|
|
27
|
+
if (!this.config.enabled) return Promise.resolve();
|
|
28
|
+
const { name, message, stack } = normalizeError(error);
|
|
29
|
+
const payload = {
|
|
30
|
+
name,
|
|
31
|
+
message,
|
|
32
|
+
stack,
|
|
33
|
+
source: isBrowser() ? "CLIENT" : "SERVER",
|
|
34
|
+
level: "ERROR",
|
|
35
|
+
environment: this.config.environment,
|
|
36
|
+
release: this.config.release,
|
|
37
|
+
data: context
|
|
38
|
+
};
|
|
39
|
+
if (isBrowser()) {
|
|
40
|
+
payload.url = window.location.href;
|
|
41
|
+
payload.userAgent = window.navigator.userAgent;
|
|
42
|
+
}
|
|
43
|
+
return this.send(payload);
|
|
44
|
+
}
|
|
45
|
+
install() {
|
|
46
|
+
if (this.installed || !this.config.enabled) return;
|
|
47
|
+
this.installed = true;
|
|
48
|
+
if (isBrowser()) {
|
|
49
|
+
window.addEventListener("error", (event) => {
|
|
50
|
+
this.captureException(_nullishCoalesce(event.error, () => ( event.message)));
|
|
51
|
+
});
|
|
52
|
+
window.addEventListener("unhandledrejection", (event) => {
|
|
53
|
+
this.captureException(_nullishCoalesce(event.reason, () => ( "Unhandled promise rejection")));
|
|
54
|
+
});
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (typeof process !== "undefined" && typeof process.on === "function") {
|
|
58
|
+
process.on("uncaughtException", (error) => this.captureException(error));
|
|
59
|
+
process.on("unhandledRejection", (reason) => this.captureException(_nullishCoalesce(reason, () => ( "Unhandled promise rejection"))));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
send(payload) {
|
|
63
|
+
return fetch(`${this.config.baseUrl}/api/v1/errors`, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: {
|
|
66
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
67
|
+
"Content-Type": "application/json"
|
|
68
|
+
},
|
|
69
|
+
body: JSON.stringify(payload)
|
|
70
|
+
}).then(async (response) => {
|
|
71
|
+
if (response.ok) return;
|
|
72
|
+
const body = await response.json().catch(() => null);
|
|
73
|
+
const errorMessage = _nullishCoalesce(_optionalChain([body, 'optionalAccess', _2 => _2.error, 'optionalAccess', _3 => _3.message]), () => ( `Request failed with status ${response.status}`));
|
|
74
|
+
console.error(`[Dataxamas] ${errorMessage}`);
|
|
75
|
+
}).catch(() => {
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// src/logs.ts
|
|
81
|
+
var _pino = require('pino');
|
|
82
|
+
var CUSTOM_LEVELS = { log: 25, success: 35 };
|
|
83
|
+
var LEVEL_METHOD = {
|
|
84
|
+
DEBUG: "debug",
|
|
85
|
+
INFO: "info",
|
|
86
|
+
WARN: "warn",
|
|
87
|
+
LOG: "log",
|
|
88
|
+
SUCCESS: "success"
|
|
89
|
+
};
|
|
90
|
+
var ANSI = {
|
|
91
|
+
DEBUG: "\x1B[90m",
|
|
92
|
+
INFO: "\x1B[36m",
|
|
93
|
+
WARN: "\x1B[33m",
|
|
94
|
+
LOG: "\x1B[37m",
|
|
95
|
+
SUCCESS: "\x1B[32m",
|
|
96
|
+
ERROR: "\x1B[31m"
|
|
97
|
+
};
|
|
98
|
+
var CSS = {
|
|
99
|
+
DEBUG: "color:#888",
|
|
100
|
+
INFO: "color:#0aa",
|
|
101
|
+
WARN: "color:#c90",
|
|
102
|
+
LOG: "color:#bbb",
|
|
103
|
+
SUCCESS: "color:#0a0",
|
|
104
|
+
ERROR: "color:#e33"
|
|
105
|
+
};
|
|
106
|
+
var RESET = "\x1B[0m";
|
|
107
|
+
function formatData(data) {
|
|
108
|
+
if (data === void 0) return "";
|
|
109
|
+
try {
|
|
110
|
+
return ` ${JSON.stringify(data)}`;
|
|
111
|
+
} catch (e3) {
|
|
112
|
+
return ` ${String(data)}`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
var nodeDestination = {
|
|
116
|
+
write(line) {
|
|
117
|
+
try {
|
|
118
|
+
const record = JSON.parse(line);
|
|
119
|
+
const level = String(_nullishCoalesce(record.level, () => ( "LOG"))).toUpperCase();
|
|
120
|
+
const color = _nullishCoalesce(ANSI[level], () => ( ""));
|
|
121
|
+
process.stdout.write(`${color}[${level}]${RESET} ${_nullishCoalesce(record.msg, () => ( ""))}${formatData(record.data)}
|
|
122
|
+
`);
|
|
123
|
+
} catch (e4) {
|
|
124
|
+
process.stdout.write(line);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
function browserWriter(level) {
|
|
129
|
+
return (record) => {
|
|
130
|
+
const method = level === "WARN" ? "warn" : "log";
|
|
131
|
+
const args = [`%c[${level}]%c ${_nullishCoalesce(record.msg, () => ( ""))}`, _nullishCoalesce(CSS[level], () => ( "")), ""];
|
|
132
|
+
if (record.data !== void 0) args.push(record.data);
|
|
133
|
+
console[method](...args);
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function createLogger() {
|
|
137
|
+
return _pino.pino.call(void 0,
|
|
138
|
+
{
|
|
139
|
+
customLevels: CUSTOM_LEVELS,
|
|
140
|
+
level: "debug",
|
|
141
|
+
base: void 0,
|
|
142
|
+
formatters: { level: (label) => ({ level: label }) },
|
|
143
|
+
browser: {
|
|
144
|
+
asObject: true,
|
|
145
|
+
write: {
|
|
146
|
+
debug: browserWriter("DEBUG"),
|
|
147
|
+
info: browserWriter("INFO"),
|
|
148
|
+
warn: browserWriter("WARN"),
|
|
149
|
+
log: browserWriter("LOG"),
|
|
150
|
+
success: browserWriter("SUCCESS"),
|
|
151
|
+
error: browserWriter("WARN")
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
nodeDestination
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
var DataxamasLogs = class {
|
|
159
|
+
constructor(config) {
|
|
160
|
+
this.config = config;
|
|
161
|
+
this.logger = createLogger();
|
|
162
|
+
}
|
|
163
|
+
debug(message, data) {
|
|
164
|
+
return this.emit("DEBUG", message, data);
|
|
165
|
+
}
|
|
166
|
+
info(message, data) {
|
|
167
|
+
return this.emit("INFO", message, data);
|
|
168
|
+
}
|
|
169
|
+
warn(message, data) {
|
|
170
|
+
return this.emit("WARN", message, data);
|
|
171
|
+
}
|
|
172
|
+
log(message, data) {
|
|
173
|
+
return this.emit("LOG", message, data);
|
|
174
|
+
}
|
|
175
|
+
success(message, data) {
|
|
176
|
+
return this.emit("SUCCESS", message, data);
|
|
177
|
+
}
|
|
178
|
+
emit(level, message, data) {
|
|
179
|
+
if (!this.config.enableLogs) return Promise.resolve();
|
|
180
|
+
const method = LEVEL_METHOD[level];
|
|
181
|
+
const fn = this.logger[method];
|
|
182
|
+
if (data === void 0) {
|
|
183
|
+
fn.call(this.logger, message);
|
|
184
|
+
} else {
|
|
185
|
+
fn.call(this.logger, { data }, message);
|
|
186
|
+
}
|
|
187
|
+
return this.send(level, message, data);
|
|
188
|
+
}
|
|
189
|
+
send(level, message, data) {
|
|
190
|
+
return fetch(`${this.config.baseUrl}/api/v1/logs`, {
|
|
191
|
+
method: "POST",
|
|
192
|
+
headers: {
|
|
193
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
194
|
+
"Content-Type": "application/json"
|
|
195
|
+
},
|
|
196
|
+
body: JSON.stringify({ level, message, data, environment: this.config.environment })
|
|
197
|
+
}).then(async (response) => {
|
|
198
|
+
if (response.ok) return;
|
|
199
|
+
const body = await response.json().catch(() => null);
|
|
200
|
+
const errorMessage = _nullishCoalesce(_optionalChain([body, 'optionalAccess', _4 => _4.error, 'optionalAccess', _5 => _5.message]), () => ( `Request failed with status ${response.status}`));
|
|
201
|
+
this.logger.error(`[Dataxamas] ${errorMessage}`);
|
|
202
|
+
}).catch((error) => {
|
|
203
|
+
this.logger.error(`[Dataxamas] Failed to send log: ${error}`);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// src/tracking.ts
|
|
209
|
+
var QUEUE_DELAY_MS = 4e3;
|
|
210
|
+
var STORAGE_PREFIX = "@dataxamas";
|
|
211
|
+
var REPLAY_FLUSH_SIZE = 100;
|
|
212
|
+
var REPLAY_FLUSH_INTERVAL_MS = 5e3;
|
|
213
|
+
var CAP_CHECK_MS = 1e3;
|
|
214
|
+
var MAX_RECORDING_MS = 10 * 60 * 1e3;
|
|
215
|
+
var MAX_EVENTS_PER_RECORDING = 2e4;
|
|
216
|
+
var KEEPALIVE_LIMIT_BYTES = 60 * 1024;
|
|
217
|
+
var REPLAY_SAMPLE_RATE = 1;
|
|
218
|
+
var DataxamasTracking = class {
|
|
219
|
+
constructor(options) {
|
|
220
|
+
/**
|
|
221
|
+
* Primeiro gesto da pageview liga o gravador; os seguintes só o religam se ele tiver parado
|
|
222
|
+
* (aba que foi para segundo plano, por exemplo), sempre no mesmo `recordingId`.
|
|
223
|
+
*/
|
|
224
|
+
this.markReplayActivity = () => {
|
|
225
|
+
if (this.replayHalted || this.replayCapped || this.isRecording()) return;
|
|
226
|
+
if (document.visibilityState === "hidden") return;
|
|
227
|
+
this.startReplayRecorder();
|
|
228
|
+
};
|
|
229
|
+
if (!options.apiKey) {
|
|
230
|
+
throw new Error("DataxamasTracking: apiKey is required.");
|
|
231
|
+
}
|
|
232
|
+
this.apiKey = options.apiKey;
|
|
233
|
+
this.baseUrl = _nullishCoalesce(options.baseUrl, () => ( BASE_URL));
|
|
234
|
+
this.allowLocalhost = _nullishCoalesce(options.allowLocalhost, () => ( false));
|
|
235
|
+
this.sessionId = null;
|
|
236
|
+
this.currentPage = isBrowser() ? window.location.pathname : "";
|
|
237
|
+
this.heatMapEnabled = _nullishCoalesce(options.heatMapEnabled, () => ( true));
|
|
238
|
+
this.replayEnabled = _nullishCoalesce(options.replayEnabled, () => ( true));
|
|
239
|
+
this.eventQueue = [];
|
|
240
|
+
this.isProcessing = false;
|
|
241
|
+
this.rrwebRecord = null;
|
|
242
|
+
this.stopRrweb = void 0;
|
|
243
|
+
this.replayBuffer = [];
|
|
244
|
+
this.snapshotFlushed = false;
|
|
245
|
+
this.replayHalted = false;
|
|
246
|
+
this.replayCapped = false;
|
|
247
|
+
this.cutoffPending = false;
|
|
248
|
+
this.replayStarting = false;
|
|
249
|
+
this.lifecycleReady = false;
|
|
250
|
+
this.recordingId = null;
|
|
251
|
+
this.recordingStartedAt = null;
|
|
252
|
+
this.recordingStartedAtMs = 0;
|
|
253
|
+
this.recordingSeq = 0;
|
|
254
|
+
this.recordingEvents = 0;
|
|
255
|
+
}
|
|
256
|
+
isLocalhost() {
|
|
257
|
+
return window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1";
|
|
258
|
+
}
|
|
259
|
+
getSessionId() {
|
|
260
|
+
try {
|
|
261
|
+
return window.localStorage.getItem(`${STORAGE_PREFIX}/session`);
|
|
262
|
+
} catch (_) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
createSessionId() {
|
|
267
|
+
const id = crypto.randomUUID();
|
|
268
|
+
try {
|
|
269
|
+
window.localStorage.setItem(`${STORAGE_PREFIX}/session`, id);
|
|
270
|
+
} catch (_) {
|
|
271
|
+
}
|
|
272
|
+
return id;
|
|
273
|
+
}
|
|
274
|
+
async processQueue() {
|
|
275
|
+
if (this.isProcessing || this.eventQueue.length === 0) return;
|
|
276
|
+
this.isProcessing = true;
|
|
277
|
+
const event = this.eventQueue.shift();
|
|
278
|
+
try {
|
|
279
|
+
await window.fetch(`${this.baseUrl}/api/v1/events`, {
|
|
280
|
+
method: "POST",
|
|
281
|
+
headers: {
|
|
282
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
283
|
+
"Content-Type": "application/json",
|
|
284
|
+
"x-screen-width": String(window.screen.width),
|
|
285
|
+
"x-screen-height": String(window.screen.height)
|
|
286
|
+
},
|
|
287
|
+
body: JSON.stringify(event)
|
|
288
|
+
});
|
|
289
|
+
} catch (_) {
|
|
290
|
+
}
|
|
291
|
+
setTimeout(() => {
|
|
292
|
+
this.isProcessing = false;
|
|
293
|
+
this.processQueue();
|
|
294
|
+
}, QUEUE_DELAY_MS);
|
|
295
|
+
}
|
|
296
|
+
async sendEvent({
|
|
297
|
+
name,
|
|
298
|
+
path = this.currentPage,
|
|
299
|
+
data = {}
|
|
300
|
+
}) {
|
|
301
|
+
if (!this.sessionId) return;
|
|
302
|
+
this.eventQueue.push({
|
|
303
|
+
name,
|
|
304
|
+
path,
|
|
305
|
+
referrer: document.referrer,
|
|
306
|
+
sessionId: this.sessionId,
|
|
307
|
+
data
|
|
308
|
+
});
|
|
309
|
+
await this.processQueue();
|
|
310
|
+
}
|
|
311
|
+
trackPageChanges() {
|
|
312
|
+
let lastPath = this.currentPage;
|
|
313
|
+
setInterval(() => {
|
|
314
|
+
const newPath = window.location.pathname;
|
|
315
|
+
if (newPath !== lastPath) {
|
|
316
|
+
this.stopReplayRecorder(false);
|
|
317
|
+
this.rotateRecording();
|
|
318
|
+
this.currentPage = newPath;
|
|
319
|
+
this.sendEvent({ name: "PAGE_VIEW", path: newPath });
|
|
320
|
+
lastPath = newPath;
|
|
321
|
+
}
|
|
322
|
+
}, 500);
|
|
323
|
+
}
|
|
324
|
+
async fetchWebsiteConfig() {
|
|
325
|
+
try {
|
|
326
|
+
const res = await window.fetch(`${this.baseUrl}/api/v1/config`, {
|
|
327
|
+
headers: { Authorization: `Bearer ${this.apiKey}` }
|
|
328
|
+
});
|
|
329
|
+
if (!res.ok) return;
|
|
330
|
+
const cfg = await res.json();
|
|
331
|
+
if (typeof cfg.heatMapEnabled === "boolean") this.heatMapEnabled = cfg.heatMapEnabled;
|
|
332
|
+
if (typeof cfg.replayEnabled === "boolean") this.replayEnabled = cfg.replayEnabled;
|
|
333
|
+
} catch (_) {
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
buildHeatMap(e) {
|
|
337
|
+
if (!this.heatMapEnabled) return null;
|
|
338
|
+
const viewportWidth = window.innerWidth;
|
|
339
|
+
const viewportHeight = window.innerHeight;
|
|
340
|
+
if (!viewportWidth || !viewportHeight) return null;
|
|
341
|
+
const doc = document.documentElement;
|
|
342
|
+
const pageWidth = Math.max(doc.scrollWidth, viewportWidth);
|
|
343
|
+
const pageHeight = Math.max(doc.scrollHeight, viewportHeight);
|
|
344
|
+
const xRatio = Math.min(1, Math.max(0, e.clientX / viewportWidth));
|
|
345
|
+
const yRatio = Math.min(1, Math.max(0, e.clientY / viewportHeight));
|
|
346
|
+
const interactionType = e.pointerType === "touch" ? "TAP" : "CLICK";
|
|
347
|
+
return {
|
|
348
|
+
interactionType,
|
|
349
|
+
xRatio,
|
|
350
|
+
yRatio,
|
|
351
|
+
viewportWidth,
|
|
352
|
+
viewportHeight,
|
|
353
|
+
pageWidth,
|
|
354
|
+
pageHeight,
|
|
355
|
+
scrollX: Math.round(window.scrollX || 0),
|
|
356
|
+
scrollY: Math.round(window.scrollY || 0),
|
|
357
|
+
layoutFingerprint: `${this.currentPage}:${Math.round(pageWidth / 100)}x${Math.round(pageHeight / 100)}`
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
safeText(t) {
|
|
361
|
+
const tag = _optionalChain([t, 'access', _6 => _6.tagName, 'optionalAccess', _7 => _7.toLowerCase, 'call', _8 => _8()]);
|
|
362
|
+
if (tag === "input" || tag === "textarea" || tag === "select" || tag === "option") return void 0;
|
|
363
|
+
if (t.isContentEditable) return void 0;
|
|
364
|
+
return _optionalChain([t, 'access', _9 => _9.textContent, 'optionalAccess', _10 => _10.trim, 'call', _11 => _11(), 'access', _12 => _12.substring, 'call', _13 => _13(0, 50)]);
|
|
365
|
+
}
|
|
366
|
+
safeString(value, max) {
|
|
367
|
+
if (typeof value !== "string") return void 0;
|
|
368
|
+
const trimmed = value.trim();
|
|
369
|
+
return trimmed ? trimmed.substring(0, max) : void 0;
|
|
370
|
+
}
|
|
371
|
+
getTarget(e) {
|
|
372
|
+
const t = e.target;
|
|
373
|
+
if (!t || t.nodeType !== 1) return null;
|
|
374
|
+
return t;
|
|
375
|
+
}
|
|
376
|
+
trackClicks() {
|
|
377
|
+
document.addEventListener(
|
|
378
|
+
"click",
|
|
379
|
+
(e) => {
|
|
380
|
+
const t = this.getTarget(e);
|
|
381
|
+
if (!t) return;
|
|
382
|
+
this.sendEvent({
|
|
383
|
+
name: "CLICK",
|
|
384
|
+
data: {
|
|
385
|
+
tagName: _optionalChain([t, 'access', _14 => _14.tagName, 'optionalAccess', _15 => _15.toLowerCase, 'call', _16 => _16()]),
|
|
386
|
+
text: this.safeText(t),
|
|
387
|
+
href: this.safeString(t.href, 2048),
|
|
388
|
+
id: this.safeString(t.id, 128),
|
|
389
|
+
classes: this.safeString(typeof t.className === "string" ? t.className : _optionalChain([t, 'access', _17 => _17.getAttribute, 'optionalCall', _18 => _18("class")]), 256),
|
|
390
|
+
heatMap: this.buildHeatMap(e)
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
},
|
|
394
|
+
true
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
trackScroll() {
|
|
398
|
+
let timeout;
|
|
399
|
+
window.addEventListener("scroll", () => {
|
|
400
|
+
clearTimeout(timeout);
|
|
401
|
+
timeout = setTimeout(() => {
|
|
402
|
+
const max = document.documentElement.scrollHeight - window.innerHeight;
|
|
403
|
+
if (max <= 0) return;
|
|
404
|
+
const percentage = Math.round(window.scrollY / max * 100);
|
|
405
|
+
this.sendEvent({
|
|
406
|
+
name: "SCROLL",
|
|
407
|
+
data: { percentage }
|
|
408
|
+
});
|
|
409
|
+
}, 1e3);
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
measureReplay() {
|
|
413
|
+
const doc = document.documentElement;
|
|
414
|
+
const viewportWidth = window.innerWidth;
|
|
415
|
+
const viewportHeight = window.innerHeight;
|
|
416
|
+
const pageWidth = Math.max(doc.scrollWidth, viewportWidth);
|
|
417
|
+
const pageHeight = Math.max(doc.scrollHeight, viewportHeight);
|
|
418
|
+
const layoutFingerprint = `${this.currentPage}:${Math.round(pageWidth / 100)}x${Math.round(pageHeight / 100)}`;
|
|
419
|
+
return { viewportWidth, viewportHeight, pageWidth, pageHeight, layoutFingerprint };
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Sorteio por visitante, não por gravação: quem entrou no sorteio tem a sessão inteira, em vez
|
|
423
|
+
* de um replay picotado que não dá para assistir.
|
|
424
|
+
*/
|
|
425
|
+
isReplaySampled() {
|
|
426
|
+
if (REPLAY_SAMPLE_RATE >= 1) return true;
|
|
427
|
+
if (REPLAY_SAMPLE_RATE <= 0) return false;
|
|
428
|
+
try {
|
|
429
|
+
const stored = window.localStorage.getItem(`${STORAGE_PREFIX}/replay-sample`);
|
|
430
|
+
if (stored === "1") return true;
|
|
431
|
+
if (stored === "0") return false;
|
|
432
|
+
const sampled = Math.random() < REPLAY_SAMPLE_RATE;
|
|
433
|
+
window.localStorage.setItem(`${STORAGE_PREFIX}/replay-sample`, sampled ? "1" : "0");
|
|
434
|
+
return sampled;
|
|
435
|
+
} catch (_) {
|
|
436
|
+
return Math.random() < REPLAY_SAMPLE_RATE;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
isRecording() {
|
|
440
|
+
return Boolean(this.stopRrweb);
|
|
441
|
+
}
|
|
442
|
+
exceededReplayCaps() {
|
|
443
|
+
return this.recordingEvents >= MAX_EVENTS_PER_RECORDING || Date.now() - this.recordingStartedAtMs >= MAX_RECORDING_MS;
|
|
444
|
+
}
|
|
445
|
+
rotateRecording() {
|
|
446
|
+
this.replayCapped = false;
|
|
447
|
+
this.recordingId = crypto.randomUUID();
|
|
448
|
+
this.recordingStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
449
|
+
this.recordingStartedAtMs = Date.now();
|
|
450
|
+
this.recordingSeq = 0;
|
|
451
|
+
this.recordingEvents = 0;
|
|
452
|
+
}
|
|
453
|
+
flushReplay(urgent) {
|
|
454
|
+
if (this.replayBuffer.length === 0 || !this.recordingId) return;
|
|
455
|
+
const events = this.replayBuffer;
|
|
456
|
+
this.replayBuffer = [];
|
|
457
|
+
const payload = JSON.stringify({
|
|
458
|
+
sessionId: this.sessionId,
|
|
459
|
+
recordingId: this.recordingId,
|
|
460
|
+
path: this.currentPage,
|
|
461
|
+
startedAt: this.recordingStartedAt,
|
|
462
|
+
seq: this.recordingSeq++,
|
|
463
|
+
mode: "REPLAY",
|
|
464
|
+
...this.measureReplay(),
|
|
465
|
+
events
|
|
466
|
+
});
|
|
467
|
+
void this.sendReplayBatch(payload, urgent);
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Um full snapshot com CSS inline passa fácil de 100KB cru, e era isso que tomava 413 no
|
|
471
|
+
* ingest. O servidor aceita os dois formatos, então navegador sem `CompressionStream` manda cru.
|
|
472
|
+
*/
|
|
473
|
+
async compressPayload(payload) {
|
|
474
|
+
if (typeof CompressionStream === "undefined") return null;
|
|
475
|
+
try {
|
|
476
|
+
const stream = new Blob([payload]).stream().pipeThrough(new CompressionStream("gzip"));
|
|
477
|
+
return await new Response(stream).blob();
|
|
478
|
+
} catch (_) {
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
async sendReplayBatch(payload, urgent) {
|
|
483
|
+
const url = `${this.baseUrl}/api/v1/replay`;
|
|
484
|
+
const rawSize = new Blob([payload]).size;
|
|
485
|
+
const compressed = urgent && rawSize < KEEPALIVE_LIMIT_BYTES ? null : await this.compressPayload(payload);
|
|
486
|
+
const headers = {
|
|
487
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
488
|
+
"Content-Type": "application/json"
|
|
489
|
+
};
|
|
490
|
+
if (compressed) headers["Content-Encoding"] = "gzip";
|
|
491
|
+
const size = compressed ? compressed.size : rawSize;
|
|
492
|
+
try {
|
|
493
|
+
const res = await window.fetch(url, {
|
|
494
|
+
method: "POST",
|
|
495
|
+
headers,
|
|
496
|
+
body: _nullishCoalesce(compressed, () => ( payload)),
|
|
497
|
+
// O flush do full snapshot sai de dentro do `emit`, no exato momento em que o visitante
|
|
498
|
+
// interage: sem `keepalive` o unload logo em seguida cancelava o POST e a gravação ficava
|
|
499
|
+
// sem o `seq` 0. O browser recusa keepalive acima de 64KB, então só liga quando cabe.
|
|
500
|
+
keepalive: size < KEEPALIVE_LIMIT_BYTES,
|
|
501
|
+
// Sem isto um embed same-origin mandaria cookie e o `*` do ingest reprovaria a resposta.
|
|
502
|
+
credentials: "omit"
|
|
503
|
+
});
|
|
504
|
+
if (!res.ok) return;
|
|
505
|
+
const body = await res.json().catch(() => null);
|
|
506
|
+
if (_optionalChain([body, 'optionalAccess', _19 => _19.stop])) {
|
|
507
|
+
this.replayHalted = true;
|
|
508
|
+
this.stopReplayRecorder(false);
|
|
509
|
+
}
|
|
510
|
+
} catch (_) {
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
stopReplayRecorder(urgent) {
|
|
514
|
+
this.flushReplay(urgent);
|
|
515
|
+
if (this.stopRrweb) {
|
|
516
|
+
this.stopRrweb();
|
|
517
|
+
this.stopRrweb = void 0;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Bateu o teto: encerra a gravação e fica parado até a próxima pageview, em vez de emendar um
|
|
522
|
+
* clipe atrás do outro enquanto a página só anima sozinha.
|
|
523
|
+
*/
|
|
524
|
+
scheduleReplayCutoff() {
|
|
525
|
+
if (this.cutoffPending) return;
|
|
526
|
+
this.cutoffPending = true;
|
|
527
|
+
this.replayCapped = true;
|
|
528
|
+
setTimeout(() => {
|
|
529
|
+
this.cutoffPending = false;
|
|
530
|
+
this.stopReplayRecorder(false);
|
|
531
|
+
}, 0);
|
|
532
|
+
}
|
|
533
|
+
setupReplayLifecycle() {
|
|
534
|
+
if (this.lifecycleReady) return;
|
|
535
|
+
this.lifecycleReady = true;
|
|
536
|
+
for (const type of ["pointerdown", "keydown", "wheel", "touchstart", "scroll"]) {
|
|
537
|
+
window.addEventListener(type, this.markReplayActivity, { passive: true, capture: true });
|
|
538
|
+
}
|
|
539
|
+
setInterval(() => {
|
|
540
|
+
if (this.isRecording()) this.flushReplay(false);
|
|
541
|
+
}, REPLAY_FLUSH_INTERVAL_MS);
|
|
542
|
+
setInterval(() => {
|
|
543
|
+
if (!this.isRecording()) return;
|
|
544
|
+
if (this.exceededReplayCaps()) this.scheduleReplayCutoff();
|
|
545
|
+
}, CAP_CHECK_MS);
|
|
546
|
+
window.addEventListener("pagehide", () => this.stopReplayRecorder(true));
|
|
547
|
+
document.addEventListener("visibilitychange", () => {
|
|
548
|
+
if (document.visibilityState === "hidden") this.stopReplayRecorder(true);
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
async startReplayRecorder() {
|
|
552
|
+
if (this.isRecording() || this.replayHalted || this.replayStarting) return;
|
|
553
|
+
this.replayStarting = true;
|
|
554
|
+
try {
|
|
555
|
+
if (!this.rrwebRecord) {
|
|
556
|
+
const { record } = await Promise.resolve().then(() => _interopRequireWildcard(require("./rrweb-M7O3D3Q3.cjs")));
|
|
557
|
+
this.rrwebRecord = record;
|
|
558
|
+
}
|
|
559
|
+
} catch (_) {
|
|
560
|
+
return;
|
|
561
|
+
} finally {
|
|
562
|
+
this.replayStarting = false;
|
|
563
|
+
}
|
|
564
|
+
if (this.isRecording() || this.replayHalted) return;
|
|
565
|
+
if (!this.recordingId) this.rotateRecording();
|
|
566
|
+
this.snapshotFlushed = false;
|
|
567
|
+
this.stopRrweb = this.rrwebRecord({
|
|
568
|
+
emit: (event) => {
|
|
569
|
+
this.replayBuffer.push(event);
|
|
570
|
+
this.recordingEvents++;
|
|
571
|
+
if (!this.snapshotFlushed && event.type === 2) {
|
|
572
|
+
this.snapshotFlushed = true;
|
|
573
|
+
this.flushReplay(false);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
if (this.exceededReplayCaps()) {
|
|
577
|
+
this.scheduleReplayCutoff();
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (this.replayBuffer.length >= REPLAY_FLUSH_SIZE) this.flushReplay(false);
|
|
581
|
+
},
|
|
582
|
+
maskAllInputs: true,
|
|
583
|
+
maskTextClass: "dx-mask",
|
|
584
|
+
blockClass: "dx-block",
|
|
585
|
+
sampling: { mousemove: 150, scroll: 300, media: 800, input: "last" },
|
|
586
|
+
inlineStylesheet: true,
|
|
587
|
+
inlineImages: false,
|
|
588
|
+
collectFonts: false,
|
|
589
|
+
slimDOMOptions: {
|
|
590
|
+
script: true,
|
|
591
|
+
comment: true,
|
|
592
|
+
headFavicon: true,
|
|
593
|
+
headMetaDescKeywords: true,
|
|
594
|
+
headMetaSocial: true,
|
|
595
|
+
headMetaRobots: true,
|
|
596
|
+
headMetaHttpEquiv: true,
|
|
597
|
+
headMetaAuthorship: true,
|
|
598
|
+
headMetaVerification: true
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
startTracking() {
|
|
603
|
+
this.sessionId = _nullishCoalesce(this.getSessionId(), () => ( this.createSessionId()));
|
|
604
|
+
this.sendEvent({ name: "PAGE_VIEW" });
|
|
605
|
+
this.trackPageChanges();
|
|
606
|
+
this.trackClicks();
|
|
607
|
+
this.trackScroll();
|
|
608
|
+
this.fetchWebsiteConfig().then(() => {
|
|
609
|
+
if (!this.replayEnabled || !this.isReplaySampled()) return;
|
|
610
|
+
this.setupReplayLifecycle();
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
start() {
|
|
614
|
+
if (!isBrowser()) {
|
|
615
|
+
throw new Error("DataxamasTracking: This SDK can only be used in a browser environment.");
|
|
616
|
+
}
|
|
617
|
+
if (!this.allowLocalhost && this.isLocalhost()) return;
|
|
618
|
+
if (document.readyState === "loading") {
|
|
619
|
+
document.addEventListener("DOMContentLoaded", this.startTracking.bind(this));
|
|
620
|
+
} else {
|
|
621
|
+
this.startTracking();
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
// src/client.ts
|
|
627
|
+
var Dataxamas = class {
|
|
628
|
+
constructor(options) {
|
|
629
|
+
if (!options.apiKey) throw new Error("Dataxamas: apiKey is required.");
|
|
630
|
+
const baseUrl = _nullishCoalesce(options.baseUrl, () => ( BASE_URL));
|
|
631
|
+
this.logs = new DataxamasLogs({
|
|
632
|
+
apiKey: options.apiKey,
|
|
633
|
+
baseUrl,
|
|
634
|
+
enableLogs: _nullishCoalesce(options.enableLogs, () => ( true)),
|
|
635
|
+
environment: options.environment
|
|
636
|
+
});
|
|
637
|
+
this.exceptions = new DataxamasExceptions({
|
|
638
|
+
apiKey: options.apiKey,
|
|
639
|
+
baseUrl,
|
|
640
|
+
enabled: _nullishCoalesce(options.enableErrorTracking, () => ( true)),
|
|
641
|
+
environment: options.environment,
|
|
642
|
+
release: options.release
|
|
643
|
+
});
|
|
644
|
+
if (_nullishCoalesce(options.captureGlobalErrors, () => ( true))) {
|
|
645
|
+
this.exceptions.install();
|
|
646
|
+
}
|
|
647
|
+
this.tracking = new DataxamasTracking({
|
|
648
|
+
apiKey: options.apiKey,
|
|
649
|
+
baseUrl,
|
|
650
|
+
replayEnabled: _nullishCoalesce(options.replayEnabled, () => ( true)),
|
|
651
|
+
heatMapEnabled: _nullishCoalesce(options.heatMapEnabled, () => ( true)),
|
|
652
|
+
allowLocalhost: _nullishCoalesce(options.allowLocalhost, () => ( false))
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
// src/errors.ts
|
|
658
|
+
var DataxamasApiError = class extends Error {
|
|
659
|
+
constructor(code, message, status) {
|
|
660
|
+
super(message);
|
|
661
|
+
this.code = code;
|
|
662
|
+
this.status = status;
|
|
663
|
+
this.name = "DataxamasApiError";
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
exports.Dataxamas = Dataxamas; exports.DataxamasApiError = DataxamasApiError; exports.DataxamasExceptions = DataxamasExceptions; exports.DataxamasLogs = DataxamasLogs; exports.DataxamasTracking = DataxamasTracking;
|