@dataxamas/tracker-js 2.0.1 → 2.1.2
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 +4 -4
- package/dist/index.cjs +222 -29
- package/dist/index.d.cts +76 -4
- package/dist/index.d.ts +76 -4
- package/dist/index.js +212 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Dataxamas SDK
|
|
2
2
|
|
|
3
|
-
Official JavaScript/TypeScript SDK for the [Dataxamas](https://dataxamas.
|
|
3
|
+
Official JavaScript/TypeScript SDK for the [Dataxamas](https://dataxamas.com) analytics platform.
|
|
4
4
|
|
|
5
5
|
One package, three capabilities:
|
|
6
6
|
|
|
@@ -96,7 +96,7 @@ const client = new Dataxamas(options);
|
|
|
96
96
|
| Option | Type | Default | Description |
|
|
97
97
|
| --- | --- | --- | --- |
|
|
98
98
|
| `apiKey` | `string` | — | **Required.** Website API key (`dx_live_`) used by logs, errors and tracking. |
|
|
99
|
-
| `baseUrl` | `string` | `https://dataxamas.
|
|
99
|
+
| `baseUrl` | `string` | `https://dataxamas.com` | API base URL. |
|
|
100
100
|
| `enableLogs` | `boolean` | `true` | Enable the logs channel. |
|
|
101
101
|
| `enableErrorTracking` | `boolean` | `true` | Enable exception capture. |
|
|
102
102
|
| `captureGlobalErrors` | `boolean` | `true` | Auto-install global error/rejection handlers. |
|
|
@@ -188,7 +188,7 @@ tracking.start();
|
|
|
188
188
|
| Option | Type | Default | Description |
|
|
189
189
|
| --- | --- | --- | --- |
|
|
190
190
|
| `apiKey` | `string` | — | **Required.** Website API key (`dx_live_`). |
|
|
191
|
-
| `baseUrl` | `string` | `https://dataxamas.
|
|
191
|
+
| `baseUrl` | `string` | `https://dataxamas.com` | API base URL. |
|
|
192
192
|
| `replayEnabled` | `boolean` | `true` | Allow session replay (gated by server config). |
|
|
193
193
|
| `heatMapEnabled` | `boolean` | `true` | Collect heat-map coordinates on clicks. |
|
|
194
194
|
| `allowLocalhost` | `boolean` | `false` | Allow tracking on `localhost`/`127.0.0.1`. |
|
|
@@ -249,7 +249,7 @@ Exported for use in your own typings:
|
|
|
249
249
|
|
|
250
250
|
## Notes
|
|
251
251
|
|
|
252
|
-
- **Default endpoint:** `https://dataxamas.
|
|
252
|
+
- **Default endpoint:** `https://dataxamas.com`. Override with `baseUrl`.
|
|
253
253
|
- **Session storage key:** `localStorage["@dataxamas/session"]`.
|
|
254
254
|
- **Event flush spacing:** 4 seconds between queued analytics events.
|
|
255
255
|
- **Replay masking classes:** `dx-mask` (text) and `dx-block` (element); all inputs masked by default.
|
package/dist/index.cjs
CHANGED
|
@@ -1,9 +1,116 @@
|
|
|
1
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.
|
|
2
|
+
var BASE_URL = "https://dataxamas.com";
|
|
3
3
|
|
|
4
4
|
// src/utils.ts
|
|
5
5
|
var isBrowser = () => typeof window !== "undefined" && typeof document !== "undefined";
|
|
6
6
|
|
|
7
|
+
// src/crawlers.ts
|
|
8
|
+
var CRAWLER_UA = /ChatGPT-User|GPTBot|OAI-SearchBot|Claude-User|Claude-SearchBot|ClaudeBot|Claude-Web|anthropic-ai|Perplexity|Gemini-Deep-Research|Googlebot|Google-InspectionTool|Storebot-Google|Google-CloudVertexBot|bingbot|BingPreview|adidxbot|MistralAI-User|DuckAssistBot|DuckDuckBot|meta-external|Applebot|Amazonbot|Bytespider|CCBot|cohere|YandexBot/i;
|
|
9
|
+
var CRAWLER_FILES = /\/(robots\.txt|llms(-full)?\.txt|sitemap[^/]*\.xml)$|\.md$/i;
|
|
10
|
+
var STATIC_ASSET = /\.(js|mjs|css|map|json|png|jpe?g|gif|webp|avif|svg|ico|woff2?|ttf|otf|eot|mp4|webm|mp3|wav|pdf|zip|txt|xml)$/i;
|
|
11
|
+
var IGNORED_PREFIXES = ["/_next/", "/assets/", "/static/", "/api/", "/_build/", "/@vite/", "/node_modules/"];
|
|
12
|
+
function isCrawlerUserAgent(userAgent) {
|
|
13
|
+
return Boolean(userAgent) && CRAWLER_UA.test(userAgent);
|
|
14
|
+
}
|
|
15
|
+
function shouldTrackPath(pathname) {
|
|
16
|
+
if (CRAWLER_FILES.test(pathname)) return true;
|
|
17
|
+
if (IGNORED_PREFIXES.some((prefix) => pathname.startsWith(prefix))) return false;
|
|
18
|
+
return !STATIC_ASSET.test(pathname);
|
|
19
|
+
}
|
|
20
|
+
function ipFromHeaders(get) {
|
|
21
|
+
const forwarded = get("x-forwarded-for");
|
|
22
|
+
if (forwarded) return _optionalChain([forwarded, 'access', _2 => _2.split, 'call', _3 => _3(","), 'access', _4 => _4[0], 'optionalAccess', _5 => _5.trim, 'call', _6 => _6()]) || null;
|
|
23
|
+
return _nullishCoalesce(_nullishCoalesce(get("cf-connecting-ip"), () => ( get("x-real-ip"))), () => ( null));
|
|
24
|
+
}
|
|
25
|
+
var DataxamasCrawlers = class {
|
|
26
|
+
constructor(config) {
|
|
27
|
+
this.config = config;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Reports a fetch-style request if it came from a known crawler. Do not await it: call it and
|
|
31
|
+
* return your response. The report is fire-and-forget and swallows every network error.
|
|
32
|
+
*/
|
|
33
|
+
trackRequest(request, options = {}) {
|
|
34
|
+
const userAgent = request.headers.get("user-agent");
|
|
35
|
+
if (!userAgent) return;
|
|
36
|
+
const promise = this.track({
|
|
37
|
+
url: request.url,
|
|
38
|
+
userAgent,
|
|
39
|
+
ip: options.ip !== void 0 ? options.ip : ipFromHeaders((name) => request.headers.get(name)),
|
|
40
|
+
statusCode: options.statusCode
|
|
41
|
+
});
|
|
42
|
+
_optionalChain([options, 'access', _7 => _7.waitUntil, 'optionalCall', _8 => _8(promise)]);
|
|
43
|
+
}
|
|
44
|
+
/** Lower-level variant for servers without a `Request` object. Resolves once sent; never rejects. */
|
|
45
|
+
track(hit) {
|
|
46
|
+
if (!this.config.enabled || isBrowser() || !isCrawlerUserAgent(hit.userAgent)) {
|
|
47
|
+
return Promise.resolve();
|
|
48
|
+
}
|
|
49
|
+
let url;
|
|
50
|
+
try {
|
|
51
|
+
url = new URL(hit.url);
|
|
52
|
+
} catch (e2) {
|
|
53
|
+
return Promise.resolve();
|
|
54
|
+
}
|
|
55
|
+
if (!shouldTrackPath(url.pathname)) return Promise.resolve();
|
|
56
|
+
url.search = "";
|
|
57
|
+
url.hash = "";
|
|
58
|
+
return fetch(`${this.config.baseUrl}/api/v1/crawls`, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: {
|
|
61
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
62
|
+
"Content-Type": "application/json"
|
|
63
|
+
},
|
|
64
|
+
body: JSON.stringify({
|
|
65
|
+
url: url.toString(),
|
|
66
|
+
userAgent: hit.userAgent,
|
|
67
|
+
ip: _nullishCoalesce(hit.ip, () => ( void 0)),
|
|
68
|
+
statusCode: hit.statusCode
|
|
69
|
+
}),
|
|
70
|
+
signal: AbortSignal.timeout(_nullishCoalesce(this.config.timeoutMs, () => ( 2e3)))
|
|
71
|
+
}).then(() => void 0).catch(() => void 0);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
function withCrawlerTracking(crawlers, handler) {
|
|
75
|
+
return async (request, ...args) => {
|
|
76
|
+
const response = await handler(request, ...args);
|
|
77
|
+
crawlers.trackRequest(request, { statusCode: response.status, waitUntil: findWaitUntil(args) });
|
|
78
|
+
return response;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function findWaitUntil(args) {
|
|
82
|
+
for (const arg of args) {
|
|
83
|
+
if (arg && typeof arg === "object" && "waitUntil" in arg && typeof arg.waitUntil === "function") {
|
|
84
|
+
const waitUntil = arg.waitUntil;
|
|
85
|
+
return (promise) => waitUntil.call(arg, promise);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return void 0;
|
|
89
|
+
}
|
|
90
|
+
function headerValue(headers, name) {
|
|
91
|
+
const value = headers[name];
|
|
92
|
+
return _nullishCoalesce((Array.isArray(value) ? value[0] : value), () => ( null));
|
|
93
|
+
}
|
|
94
|
+
function createExpressCrawlerMiddleware(crawlers) {
|
|
95
|
+
return (request, response, next) => {
|
|
96
|
+
const userAgent = headerValue(request.headers, "user-agent");
|
|
97
|
+
if (isCrawlerUserAgent(userAgent)) {
|
|
98
|
+
const host = _nullishCoalesce(headerValue(request.headers, "x-forwarded-host"), () => ( headerValue(request.headers, "host")));
|
|
99
|
+
const protocol = _nullishCoalesce(request.protocol, () => ( "https"));
|
|
100
|
+
const path = _nullishCoalesce(_nullishCoalesce(request.originalUrl, () => ( request.url)), () => ( "/"));
|
|
101
|
+
response.once("finish", () => {
|
|
102
|
+
void crawlers.track({
|
|
103
|
+
url: `${protocol}://${host}${path}`,
|
|
104
|
+
userAgent,
|
|
105
|
+
ip: _nullishCoalesce(_nullishCoalesce(_nullishCoalesce(request.ip, () => ( ipFromHeaders((name) => headerValue(request.headers, name)))), () => ( _optionalChain([request, 'access', _9 => _9.socket, 'optionalAccess', _10 => _10.remoteAddress]))), () => ( null)),
|
|
106
|
+
statusCode: response.statusCode
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
next();
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
7
114
|
// src/exceptions.ts
|
|
8
115
|
function normalizeError(error) {
|
|
9
116
|
if (error instanceof Error) {
|
|
@@ -14,7 +121,7 @@ function normalizeError(error) {
|
|
|
14
121
|
}
|
|
15
122
|
try {
|
|
16
123
|
return { name: "Error", message: JSON.stringify(error) };
|
|
17
|
-
} catch (
|
|
124
|
+
} catch (e3) {
|
|
18
125
|
return { name: "Error", message: String(error) };
|
|
19
126
|
}
|
|
20
127
|
}
|
|
@@ -70,7 +177,7 @@ var DataxamasExceptions = class {
|
|
|
70
177
|
}).then(async (response) => {
|
|
71
178
|
if (response.ok) return;
|
|
72
179
|
const body = await response.json().catch(() => null);
|
|
73
|
-
const errorMessage = _nullishCoalesce(_optionalChain([body, 'optionalAccess',
|
|
180
|
+
const errorMessage = _nullishCoalesce(_optionalChain([body, 'optionalAccess', _11 => _11.error, 'optionalAccess', _12 => _12.message]), () => ( `Request failed with status ${response.status}`));
|
|
74
181
|
console.error(`[Dataxamas] ${errorMessage}`);
|
|
75
182
|
}).catch(() => {
|
|
76
183
|
});
|
|
@@ -108,7 +215,7 @@ function formatData(data) {
|
|
|
108
215
|
if (data === void 0) return "";
|
|
109
216
|
try {
|
|
110
217
|
return ` ${JSON.stringify(data)}`;
|
|
111
|
-
} catch (
|
|
218
|
+
} catch (e4) {
|
|
112
219
|
return ` ${String(data)}`;
|
|
113
220
|
}
|
|
114
221
|
}
|
|
@@ -120,7 +227,7 @@ var nodeDestination = {
|
|
|
120
227
|
const color = _nullishCoalesce(ANSI[level], () => ( ""));
|
|
121
228
|
process.stdout.write(`${color}[${level}]${RESET} ${_nullishCoalesce(record.msg, () => ( ""))}${formatData(record.data)}
|
|
122
229
|
`);
|
|
123
|
-
} catch (
|
|
230
|
+
} catch (e5) {
|
|
124
231
|
process.stdout.write(line);
|
|
125
232
|
}
|
|
126
233
|
}
|
|
@@ -197,7 +304,7 @@ var DataxamasLogs = class {
|
|
|
197
304
|
}).then(async (response) => {
|
|
198
305
|
if (response.ok) return;
|
|
199
306
|
const body = await response.json().catch(() => null);
|
|
200
|
-
const errorMessage = _nullishCoalesce(_optionalChain([body, 'optionalAccess',
|
|
307
|
+
const errorMessage = _nullishCoalesce(_optionalChain([body, 'optionalAccess', _13 => _13.error, 'optionalAccess', _14 => _14.message]), () => ( `Request failed with status ${response.status}`));
|
|
201
308
|
this.logger.error(`[Dataxamas] ${errorMessage}`);
|
|
202
309
|
}).catch((error) => {
|
|
203
310
|
this.logger.error(`[Dataxamas] Failed to send log: ${error}`);
|
|
@@ -211,19 +318,34 @@ var STORAGE_PREFIX = "@dataxamas";
|
|
|
211
318
|
var REPLAY_FLUSH_SIZE = 100;
|
|
212
319
|
var REPLAY_FLUSH_INTERVAL_MS = 5e3;
|
|
213
320
|
var CAP_CHECK_MS = 1e3;
|
|
214
|
-
var MAX_RECORDING_MS =
|
|
215
|
-
var MAX_EVENTS_PER_RECORDING =
|
|
321
|
+
var MAX_RECORDING_MS = 30 * 60 * 1e3;
|
|
322
|
+
var MAX_EVENTS_PER_RECORDING = 5e4;
|
|
323
|
+
var REPLAY_IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
324
|
+
var ACTIVITY_SAVE_THROTTLE_MS = 5e3;
|
|
325
|
+
var REPLAY_RECORDING_KEY = `${STORAGE_PREFIX}/replay-recording`;
|
|
326
|
+
function isStoredRecording(value) {
|
|
327
|
+
if (typeof value !== "object" || value === null) return false;
|
|
328
|
+
const stored = value;
|
|
329
|
+
return typeof stored.id === "string" && typeof stored.sessionId === "string" && typeof stored.startedAt === "string" && typeof stored.startedAtMs === "number" && typeof stored.seq === "number" && typeof stored.events === "number" && typeof stored.lastActivityAt === "number" && typeof stored.capped === "boolean";
|
|
330
|
+
}
|
|
216
331
|
var KEEPALIVE_LIMIT_BYTES = 60 * 1024;
|
|
217
|
-
var REPLAY_SAMPLE_RATE = 1;
|
|
218
332
|
var DataxamasTracking = class {
|
|
219
333
|
constructor(options) {
|
|
220
334
|
/**
|
|
221
|
-
* Primeiro gesto
|
|
222
|
-
*
|
|
335
|
+
* Primeiro gesto liga o gravador; os seguintes só o religam se ele tiver parado (aba que foi
|
|
336
|
+
* para segundo plano, reload), no mesmo `recordingId`. Depois do teto ou de muito tempo parado,
|
|
337
|
+
* o gesto abre uma gravação nova.
|
|
223
338
|
*/
|
|
224
339
|
this.markReplayActivity = () => {
|
|
225
|
-
if (this.replayHalted || this.
|
|
340
|
+
if (this.replayHalted || this.cutoffPending) return;
|
|
226
341
|
if (document.visibilityState === "hidden") return;
|
|
342
|
+
const now = Date.now();
|
|
343
|
+
if (this.replayCapped || now - this.lastActivityAt >= REPLAY_IDLE_TIMEOUT_MS) {
|
|
344
|
+
if (this.isRecording()) this.stopReplayRecorder(false);
|
|
345
|
+
this.rotateRecording();
|
|
346
|
+
}
|
|
347
|
+
this.touchActivity(now);
|
|
348
|
+
if (this.isRecording()) return;
|
|
227
349
|
this.startReplayRecorder();
|
|
228
350
|
};
|
|
229
351
|
if (!options.apiKey) {
|
|
@@ -236,6 +358,7 @@ var DataxamasTracking = class {
|
|
|
236
358
|
this.currentPage = isBrowser() ? window.location.pathname : "";
|
|
237
359
|
this.heatMapEnabled = _nullishCoalesce(options.heatMapEnabled, () => ( true));
|
|
238
360
|
this.replayEnabled = _nullishCoalesce(options.replayEnabled, () => ( true));
|
|
361
|
+
this.replaySampleRate = 1;
|
|
239
362
|
this.eventQueue = [];
|
|
240
363
|
this.isProcessing = false;
|
|
241
364
|
this.rrwebRecord = null;
|
|
@@ -252,6 +375,8 @@ var DataxamasTracking = class {
|
|
|
252
375
|
this.recordingStartedAtMs = 0;
|
|
253
376
|
this.recordingSeq = 0;
|
|
254
377
|
this.recordingEvents = 0;
|
|
378
|
+
this.lastActivityAt = 0;
|
|
379
|
+
this.lastActivitySavedAt = 0;
|
|
255
380
|
}
|
|
256
381
|
isLocalhost() {
|
|
257
382
|
return window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1";
|
|
@@ -313,8 +438,7 @@ var DataxamasTracking = class {
|
|
|
313
438
|
setInterval(() => {
|
|
314
439
|
const newPath = window.location.pathname;
|
|
315
440
|
if (newPath !== lastPath) {
|
|
316
|
-
this.
|
|
317
|
-
this.rotateRecording();
|
|
441
|
+
this.flushReplay(false);
|
|
318
442
|
this.currentPage = newPath;
|
|
319
443
|
this.sendEvent({ name: "PAGE_VIEW", path: newPath });
|
|
320
444
|
lastPath = newPath;
|
|
@@ -330,6 +454,9 @@ var DataxamasTracking = class {
|
|
|
330
454
|
const cfg = await res.json();
|
|
331
455
|
if (typeof cfg.heatMapEnabled === "boolean") this.heatMapEnabled = cfg.heatMapEnabled;
|
|
332
456
|
if (typeof cfg.replayEnabled === "boolean") this.replayEnabled = cfg.replayEnabled;
|
|
457
|
+
if (typeof cfg.replaySampleRate === "number" && cfg.replaySampleRate >= 0 && cfg.replaySampleRate <= 1) {
|
|
458
|
+
this.replaySampleRate = cfg.replaySampleRate;
|
|
459
|
+
}
|
|
333
460
|
} catch (_) {
|
|
334
461
|
}
|
|
335
462
|
}
|
|
@@ -358,10 +485,10 @@ var DataxamasTracking = class {
|
|
|
358
485
|
};
|
|
359
486
|
}
|
|
360
487
|
safeText(t) {
|
|
361
|
-
const tag = _optionalChain([t, 'access',
|
|
488
|
+
const tag = _optionalChain([t, 'access', _15 => _15.tagName, 'optionalAccess', _16 => _16.toLowerCase, 'call', _17 => _17()]);
|
|
362
489
|
if (tag === "input" || tag === "textarea" || tag === "select" || tag === "option") return void 0;
|
|
363
490
|
if (t.isContentEditable) return void 0;
|
|
364
|
-
return _optionalChain([t, 'access',
|
|
491
|
+
return _optionalChain([t, 'access', _18 => _18.textContent, 'optionalAccess', _19 => _19.trim, 'call', _20 => _20(), 'access', _21 => _21.substring, 'call', _22 => _22(0, 50)]);
|
|
365
492
|
}
|
|
366
493
|
safeString(value, max) {
|
|
367
494
|
if (typeof value !== "string") return void 0;
|
|
@@ -382,11 +509,11 @@ var DataxamasTracking = class {
|
|
|
382
509
|
this.sendEvent({
|
|
383
510
|
name: "CLICK",
|
|
384
511
|
data: {
|
|
385
|
-
tagName: _optionalChain([t, 'access',
|
|
512
|
+
tagName: _optionalChain([t, 'access', _23 => _23.tagName, 'optionalAccess', _24 => _24.toLowerCase, 'call', _25 => _25()]),
|
|
386
513
|
text: this.safeText(t),
|
|
387
514
|
href: this.safeString(t.href, 2048),
|
|
388
515
|
id: this.safeString(t.id, 128),
|
|
389
|
-
classes: this.safeString(typeof t.className === "string" ? t.className : _optionalChain([t, 'access',
|
|
516
|
+
classes: this.safeString(typeof t.className === "string" ? t.className : _optionalChain([t, 'access', _26 => _26.getAttribute, 'optionalCall', _27 => _27("class")]), 256),
|
|
390
517
|
heatMap: this.buildHeatMap(e)
|
|
391
518
|
}
|
|
392
519
|
});
|
|
@@ -423,17 +550,20 @@ var DataxamasTracking = class {
|
|
|
423
550
|
* de um replay picotado que não dá para assistir.
|
|
424
551
|
*/
|
|
425
552
|
isReplaySampled() {
|
|
426
|
-
|
|
427
|
-
if (
|
|
553
|
+
const rate = this.replaySampleRate;
|
|
554
|
+
if (rate >= 1) return true;
|
|
555
|
+
if (rate <= 0) return false;
|
|
556
|
+
const key = `${STORAGE_PREFIX}/replay-sample`;
|
|
428
557
|
try {
|
|
429
|
-
const
|
|
430
|
-
if (
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
558
|
+
const [storedRate, storedDecision] = (_nullishCoalesce(window.localStorage.getItem(key), () => ( ""))).split(":");
|
|
559
|
+
if (Number(storedRate) === rate && (storedDecision === "1" || storedDecision === "0")) {
|
|
560
|
+
return storedDecision === "1";
|
|
561
|
+
}
|
|
562
|
+
const sampled = Math.random() < rate;
|
|
563
|
+
window.localStorage.setItem(key, `${rate}:${sampled ? "1" : "0"}`);
|
|
434
564
|
return sampled;
|
|
435
565
|
} catch (_) {
|
|
436
|
-
return Math.random() <
|
|
566
|
+
return Math.random() < rate;
|
|
437
567
|
}
|
|
438
568
|
}
|
|
439
569
|
isRecording() {
|
|
@@ -442,6 +572,31 @@ var DataxamasTracking = class {
|
|
|
442
572
|
exceededReplayCaps() {
|
|
443
573
|
return this.recordingEvents >= MAX_EVENTS_PER_RECORDING || Date.now() - this.recordingStartedAtMs >= MAX_RECORDING_MS;
|
|
444
574
|
}
|
|
575
|
+
loadRecording() {
|
|
576
|
+
try {
|
|
577
|
+
const parsed = JSON.parse(_nullishCoalesce(window.sessionStorage.getItem(REPLAY_RECORDING_KEY), () => ( "null")));
|
|
578
|
+
return isStoredRecording(parsed) ? parsed : null;
|
|
579
|
+
} catch (_) {
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
saveRecording() {
|
|
584
|
+
if (!this.sessionId || !this.recordingId || !this.recordingStartedAt) return;
|
|
585
|
+
const stored = {
|
|
586
|
+
id: this.recordingId,
|
|
587
|
+
sessionId: this.sessionId,
|
|
588
|
+
startedAt: this.recordingStartedAt,
|
|
589
|
+
startedAtMs: this.recordingStartedAtMs,
|
|
590
|
+
seq: this.recordingSeq,
|
|
591
|
+
events: this.recordingEvents,
|
|
592
|
+
lastActivityAt: this.lastActivityAt,
|
|
593
|
+
capped: this.replayCapped
|
|
594
|
+
};
|
|
595
|
+
try {
|
|
596
|
+
window.sessionStorage.setItem(REPLAY_RECORDING_KEY, JSON.stringify(stored));
|
|
597
|
+
} catch (_) {
|
|
598
|
+
}
|
|
599
|
+
}
|
|
445
600
|
rotateRecording() {
|
|
446
601
|
this.replayCapped = false;
|
|
447
602
|
this.recordingId = crypto.randomUUID();
|
|
@@ -449,6 +604,29 @@ var DataxamasTracking = class {
|
|
|
449
604
|
this.recordingStartedAtMs = Date.now();
|
|
450
605
|
this.recordingSeq = 0;
|
|
451
606
|
this.recordingEvents = 0;
|
|
607
|
+
this.lastActivityAt = Date.now();
|
|
608
|
+
this.saveRecording();
|
|
609
|
+
}
|
|
610
|
+
/** Reload ou navegação completa na mesma aba continua a gravação, se a visita ainda estiver ativa. */
|
|
611
|
+
resumeOrRotateRecording() {
|
|
612
|
+
const stored = this.loadRecording();
|
|
613
|
+
if (!stored || stored.capped || stored.sessionId !== this.sessionId || Date.now() - stored.lastActivityAt >= REPLAY_IDLE_TIMEOUT_MS) {
|
|
614
|
+
this.rotateRecording();
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
this.replayCapped = false;
|
|
618
|
+
this.recordingId = stored.id;
|
|
619
|
+
this.recordingStartedAt = stored.startedAt;
|
|
620
|
+
this.recordingStartedAtMs = stored.startedAtMs;
|
|
621
|
+
this.recordingSeq = stored.seq;
|
|
622
|
+
this.recordingEvents = stored.events;
|
|
623
|
+
this.lastActivityAt = stored.lastActivityAt;
|
|
624
|
+
}
|
|
625
|
+
touchActivity(now) {
|
|
626
|
+
this.lastActivityAt = now;
|
|
627
|
+
if (now - this.lastActivitySavedAt < ACTIVITY_SAVE_THROTTLE_MS) return;
|
|
628
|
+
this.lastActivitySavedAt = now;
|
|
629
|
+
this.saveRecording();
|
|
452
630
|
}
|
|
453
631
|
flushReplay(urgent) {
|
|
454
632
|
if (this.replayBuffer.length === 0 || !this.recordingId) return;
|
|
@@ -464,6 +642,7 @@ var DataxamasTracking = class {
|
|
|
464
642
|
...this.measureReplay(),
|
|
465
643
|
events
|
|
466
644
|
});
|
|
645
|
+
this.saveRecording();
|
|
467
646
|
void this.sendReplayBatch(payload, urgent);
|
|
468
647
|
}
|
|
469
648
|
/**
|
|
@@ -503,8 +682,10 @@ var DataxamasTracking = class {
|
|
|
503
682
|
});
|
|
504
683
|
if (!res.ok) return;
|
|
505
684
|
const body = await res.json().catch(() => null);
|
|
506
|
-
if (_optionalChain([body, 'optionalAccess',
|
|
685
|
+
if (_optionalChain([body, 'optionalAccess', _28 => _28.stop])) {
|
|
507
686
|
this.replayHalted = true;
|
|
687
|
+
this.replayCapped = true;
|
|
688
|
+
this.saveRecording();
|
|
508
689
|
this.stopReplayRecorder(false);
|
|
509
690
|
}
|
|
510
691
|
} catch (_) {
|
|
@@ -518,13 +699,14 @@ var DataxamasTracking = class {
|
|
|
518
699
|
}
|
|
519
700
|
}
|
|
520
701
|
/**
|
|
521
|
-
* Bateu o teto: encerra a gravação e fica parado até a próxima
|
|
702
|
+
* Bateu o teto: encerra a gravação e fica parado até a próxima interação, em vez de emendar um
|
|
522
703
|
* clipe atrás do outro enquanto a página só anima sozinha.
|
|
523
704
|
*/
|
|
524
705
|
scheduleReplayCutoff() {
|
|
525
706
|
if (this.cutoffPending) return;
|
|
526
707
|
this.cutoffPending = true;
|
|
527
708
|
this.replayCapped = true;
|
|
709
|
+
this.saveRecording();
|
|
528
710
|
setTimeout(() => {
|
|
529
711
|
this.cutoffPending = false;
|
|
530
712
|
this.stopReplayRecorder(false);
|
|
@@ -601,6 +783,7 @@ var DataxamasTracking = class {
|
|
|
601
783
|
}
|
|
602
784
|
startTracking() {
|
|
603
785
|
this.sessionId = _nullishCoalesce(this.getSessionId(), () => ( this.createSessionId()));
|
|
786
|
+
this.resumeOrRotateRecording();
|
|
604
787
|
this.sendEvent({ name: "PAGE_VIEW" });
|
|
605
788
|
this.trackPageChanges();
|
|
606
789
|
this.trackClicks();
|
|
@@ -615,6 +798,7 @@ var DataxamasTracking = class {
|
|
|
615
798
|
throw new Error("DataxamasTracking: This SDK can only be used in a browser environment.");
|
|
616
799
|
}
|
|
617
800
|
if (!this.allowLocalhost && this.isLocalhost()) return;
|
|
801
|
+
if (navigator.webdriver) return;
|
|
618
802
|
if (document.readyState === "loading") {
|
|
619
803
|
document.addEventListener("DOMContentLoaded", this.startTracking.bind(this));
|
|
620
804
|
} else {
|
|
@@ -651,6 +835,11 @@ var Dataxamas = class {
|
|
|
651
835
|
heatMapEnabled: _nullishCoalesce(options.heatMapEnabled, () => ( true)),
|
|
652
836
|
allowLocalhost: _nullishCoalesce(options.allowLocalhost, () => ( false))
|
|
653
837
|
});
|
|
838
|
+
this.crawlers = new DataxamasCrawlers({
|
|
839
|
+
apiKey: options.apiKey,
|
|
840
|
+
baseUrl,
|
|
841
|
+
enabled: _nullishCoalesce(options.enableCrawlerTracking, () => ( true))
|
|
842
|
+
});
|
|
654
843
|
}
|
|
655
844
|
};
|
|
656
845
|
|
|
@@ -669,4 +858,8 @@ var DataxamasApiError = class extends Error {
|
|
|
669
858
|
|
|
670
859
|
|
|
671
860
|
|
|
672
|
-
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
exports.Dataxamas = Dataxamas; exports.DataxamasApiError = DataxamasApiError; exports.DataxamasCrawlers = DataxamasCrawlers; exports.DataxamasExceptions = DataxamasExceptions; exports.DataxamasLogs = DataxamasLogs; exports.DataxamasTracking = DataxamasTracking; exports.createExpressCrawlerMiddleware = createExpressCrawlerMiddleware; exports.isCrawlerUserAgent = isCrawlerUserAgent; exports.withCrawlerTracking = withCrawlerTracking;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,3 +1,64 @@
|
|
|
1
|
+
interface CrawlersConfig {
|
|
2
|
+
apiKey: string;
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
/** Abort the report after this long. The crawler's response never waits on it either way. */
|
|
6
|
+
timeoutMs?: number;
|
|
7
|
+
}
|
|
8
|
+
interface CrawlHit {
|
|
9
|
+
/** Absolute URL the crawler requested. The query string is dropped before sending. */
|
|
10
|
+
url: string;
|
|
11
|
+
userAgent: string;
|
|
12
|
+
/** Crawler's source IP, used to verify it against the provider's published ranges. */
|
|
13
|
+
ip?: string | null;
|
|
14
|
+
statusCode?: number;
|
|
15
|
+
}
|
|
16
|
+
interface TrackCrawlOptions {
|
|
17
|
+
statusCode?: number;
|
|
18
|
+
/** Overrides the IP read from `x-forwarded-for` / `x-real-ip` / `cf-connecting-ip`. */
|
|
19
|
+
ip?: string | null;
|
|
20
|
+
/** Keeps the runtime alive until the report is sent (Workers, Vercel, Next `event.waitUntil`). */
|
|
21
|
+
waitUntil?: (promise: Promise<unknown>) => void;
|
|
22
|
+
}
|
|
23
|
+
declare function isCrawlerUserAgent(userAgent: string | null | undefined): boolean;
|
|
24
|
+
declare class DataxamasCrawlers {
|
|
25
|
+
private readonly config;
|
|
26
|
+
constructor(config: CrawlersConfig);
|
|
27
|
+
/**
|
|
28
|
+
* Reports a fetch-style request if it came from a known crawler. Do not await it: call it and
|
|
29
|
+
* return your response. The report is fire-and-forget and swallows every network error.
|
|
30
|
+
*/
|
|
31
|
+
trackRequest(request: Request, options?: TrackCrawlOptions): void;
|
|
32
|
+
/** Lower-level variant for servers without a `Request` object. Resolves once sent; never rejects. */
|
|
33
|
+
track(hit: CrawlHit): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Wraps a fetch-style handler (Cloudflare Workers, Hono, TanStack Start, Bun.serve) so every
|
|
37
|
+
* crawler request is reported with its final status code. Any argument carrying `waitUntil`
|
|
38
|
+
* (the Workers `ctx`, Next's `event`) is used to keep the runtime alive for the report.
|
|
39
|
+
*/
|
|
40
|
+
declare function withCrawlerTracking<Args extends unknown[]>(crawlers: DataxamasCrawlers, handler: (request: Request, ...args: Args) => Response | Promise<Response>): (request: Request, ...args: Args) => Promise<Response>;
|
|
41
|
+
interface ExpressLikeRequest {
|
|
42
|
+
headers: Record<string, string | string[] | undefined>;
|
|
43
|
+
originalUrl?: string;
|
|
44
|
+
url?: string;
|
|
45
|
+
protocol?: string;
|
|
46
|
+
ip?: string;
|
|
47
|
+
socket?: {
|
|
48
|
+
remoteAddress?: string;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
interface ExpressLikeResponse {
|
|
52
|
+
statusCode: number;
|
|
53
|
+
once(event: "finish", listener: () => void): unknown;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Express / Connect middleware. It reports after the response finishes, so the status code is
|
|
57
|
+
* the real one, and never delays `next()`. With `app.set("trust proxy", ...)` configured,
|
|
58
|
+
* `req.ip` already holds the client IP and is preferred over the raw headers.
|
|
59
|
+
*/
|
|
60
|
+
declare function createExpressCrawlerMiddleware(crawlers: DataxamasCrawlers): (request: ExpressLikeRequest, response: ExpressLikeResponse, next: () => void) => void;
|
|
61
|
+
|
|
1
62
|
type ErrorSource = "CLIENT" | "SERVER";
|
|
2
63
|
type ErrorLevel = "ERROR" | "WARNING" | "FATAL";
|
|
3
64
|
interface ExceptionsConfig {
|
|
@@ -51,6 +112,7 @@ declare class DataxamasTracking {
|
|
|
51
112
|
private currentPage;
|
|
52
113
|
private heatMapEnabled;
|
|
53
114
|
private replayEnabled;
|
|
115
|
+
private replaySampleRate;
|
|
54
116
|
private eventQueue;
|
|
55
117
|
private isProcessing;
|
|
56
118
|
private rrwebRecord;
|
|
@@ -67,6 +129,8 @@ declare class DataxamasTracking {
|
|
|
67
129
|
private recordingStartedAtMs;
|
|
68
130
|
private recordingSeq;
|
|
69
131
|
private recordingEvents;
|
|
132
|
+
private lastActivityAt;
|
|
133
|
+
private lastActivitySavedAt;
|
|
70
134
|
constructor(options: DataxamasTrackingOptions);
|
|
71
135
|
private isLocalhost;
|
|
72
136
|
private getSessionId;
|
|
@@ -89,7 +153,12 @@ declare class DataxamasTracking {
|
|
|
89
153
|
private isReplaySampled;
|
|
90
154
|
private isRecording;
|
|
91
155
|
private exceededReplayCaps;
|
|
156
|
+
private loadRecording;
|
|
157
|
+
private saveRecording;
|
|
92
158
|
private rotateRecording;
|
|
159
|
+
/** Reload ou navegação completa na mesma aba continua a gravação, se a visita ainda estiver ativa. */
|
|
160
|
+
private resumeOrRotateRecording;
|
|
161
|
+
private touchActivity;
|
|
93
162
|
private flushReplay;
|
|
94
163
|
/**
|
|
95
164
|
* Um full snapshot com CSS inline passa fácil de 100KB cru, e era isso que tomava 413 no
|
|
@@ -99,13 +168,14 @@ declare class DataxamasTracking {
|
|
|
99
168
|
private sendReplayBatch;
|
|
100
169
|
private stopReplayRecorder;
|
|
101
170
|
/**
|
|
102
|
-
* Bateu o teto: encerra a gravação e fica parado até a próxima
|
|
171
|
+
* Bateu o teto: encerra a gravação e fica parado até a próxima interação, em vez de emendar um
|
|
103
172
|
* clipe atrás do outro enquanto a página só anima sozinha.
|
|
104
173
|
*/
|
|
105
174
|
private scheduleReplayCutoff;
|
|
106
175
|
/**
|
|
107
|
-
* Primeiro gesto
|
|
108
|
-
*
|
|
176
|
+
* Primeiro gesto liga o gravador; os seguintes só o religam se ele tiver parado (aba que foi
|
|
177
|
+
* para segundo plano, reload), no mesmo `recordingId`. Depois do teto ou de muito tempo parado,
|
|
178
|
+
* o gesto abre uma gravação nova.
|
|
109
179
|
*/
|
|
110
180
|
private markReplayActivity;
|
|
111
181
|
private setupReplayLifecycle;
|
|
@@ -122,6 +192,7 @@ interface DataxamasOptions {
|
|
|
122
192
|
release?: string;
|
|
123
193
|
enableLogs?: boolean;
|
|
124
194
|
enableErrorTracking?: boolean;
|
|
195
|
+
enableCrawlerTracking?: boolean;
|
|
125
196
|
captureGlobalErrors?: boolean;
|
|
126
197
|
replayEnabled?: boolean;
|
|
127
198
|
heatMapEnabled?: boolean;
|
|
@@ -130,6 +201,7 @@ declare class Dataxamas {
|
|
|
130
201
|
readonly logs: DataxamasLogs;
|
|
131
202
|
readonly exceptions: DataxamasExceptions;
|
|
132
203
|
readonly tracking: DataxamasTracking;
|
|
204
|
+
readonly crawlers: DataxamasCrawlers;
|
|
133
205
|
constructor(options: DataxamasOptions);
|
|
134
206
|
}
|
|
135
207
|
|
|
@@ -139,4 +211,4 @@ declare class DataxamasApiError extends Error {
|
|
|
139
211
|
constructor(code: string, message: string, status: number);
|
|
140
212
|
}
|
|
141
213
|
|
|
142
|
-
export { Dataxamas, DataxamasApiError, DataxamasExceptions, DataxamasLogs, type DataxamasOptions, DataxamasTracking, type DataxamasTrackingOptions, type ErrorLevel, type ErrorSource, type ExceptionsConfig, type LogLevel, type LogsConfig };
|
|
214
|
+
export { type CrawlHit, type CrawlersConfig, Dataxamas, DataxamasApiError, DataxamasCrawlers, DataxamasExceptions, DataxamasLogs, type DataxamasOptions, DataxamasTracking, type DataxamasTrackingOptions, type ErrorLevel, type ErrorSource, type ExceptionsConfig, type LogLevel, type LogsConfig, type TrackCrawlOptions, createExpressCrawlerMiddleware, isCrawlerUserAgent, withCrawlerTracking };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,64 @@
|
|
|
1
|
+
interface CrawlersConfig {
|
|
2
|
+
apiKey: string;
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
/** Abort the report after this long. The crawler's response never waits on it either way. */
|
|
6
|
+
timeoutMs?: number;
|
|
7
|
+
}
|
|
8
|
+
interface CrawlHit {
|
|
9
|
+
/** Absolute URL the crawler requested. The query string is dropped before sending. */
|
|
10
|
+
url: string;
|
|
11
|
+
userAgent: string;
|
|
12
|
+
/** Crawler's source IP, used to verify it against the provider's published ranges. */
|
|
13
|
+
ip?: string | null;
|
|
14
|
+
statusCode?: number;
|
|
15
|
+
}
|
|
16
|
+
interface TrackCrawlOptions {
|
|
17
|
+
statusCode?: number;
|
|
18
|
+
/** Overrides the IP read from `x-forwarded-for` / `x-real-ip` / `cf-connecting-ip`. */
|
|
19
|
+
ip?: string | null;
|
|
20
|
+
/** Keeps the runtime alive until the report is sent (Workers, Vercel, Next `event.waitUntil`). */
|
|
21
|
+
waitUntil?: (promise: Promise<unknown>) => void;
|
|
22
|
+
}
|
|
23
|
+
declare function isCrawlerUserAgent(userAgent: string | null | undefined): boolean;
|
|
24
|
+
declare class DataxamasCrawlers {
|
|
25
|
+
private readonly config;
|
|
26
|
+
constructor(config: CrawlersConfig);
|
|
27
|
+
/**
|
|
28
|
+
* Reports a fetch-style request if it came from a known crawler. Do not await it: call it and
|
|
29
|
+
* return your response. The report is fire-and-forget and swallows every network error.
|
|
30
|
+
*/
|
|
31
|
+
trackRequest(request: Request, options?: TrackCrawlOptions): void;
|
|
32
|
+
/** Lower-level variant for servers without a `Request` object. Resolves once sent; never rejects. */
|
|
33
|
+
track(hit: CrawlHit): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Wraps a fetch-style handler (Cloudflare Workers, Hono, TanStack Start, Bun.serve) so every
|
|
37
|
+
* crawler request is reported with its final status code. Any argument carrying `waitUntil`
|
|
38
|
+
* (the Workers `ctx`, Next's `event`) is used to keep the runtime alive for the report.
|
|
39
|
+
*/
|
|
40
|
+
declare function withCrawlerTracking<Args extends unknown[]>(crawlers: DataxamasCrawlers, handler: (request: Request, ...args: Args) => Response | Promise<Response>): (request: Request, ...args: Args) => Promise<Response>;
|
|
41
|
+
interface ExpressLikeRequest {
|
|
42
|
+
headers: Record<string, string | string[] | undefined>;
|
|
43
|
+
originalUrl?: string;
|
|
44
|
+
url?: string;
|
|
45
|
+
protocol?: string;
|
|
46
|
+
ip?: string;
|
|
47
|
+
socket?: {
|
|
48
|
+
remoteAddress?: string;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
interface ExpressLikeResponse {
|
|
52
|
+
statusCode: number;
|
|
53
|
+
once(event: "finish", listener: () => void): unknown;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Express / Connect middleware. It reports after the response finishes, so the status code is
|
|
57
|
+
* the real one, and never delays `next()`. With `app.set("trust proxy", ...)` configured,
|
|
58
|
+
* `req.ip` already holds the client IP and is preferred over the raw headers.
|
|
59
|
+
*/
|
|
60
|
+
declare function createExpressCrawlerMiddleware(crawlers: DataxamasCrawlers): (request: ExpressLikeRequest, response: ExpressLikeResponse, next: () => void) => void;
|
|
61
|
+
|
|
1
62
|
type ErrorSource = "CLIENT" | "SERVER";
|
|
2
63
|
type ErrorLevel = "ERROR" | "WARNING" | "FATAL";
|
|
3
64
|
interface ExceptionsConfig {
|
|
@@ -51,6 +112,7 @@ declare class DataxamasTracking {
|
|
|
51
112
|
private currentPage;
|
|
52
113
|
private heatMapEnabled;
|
|
53
114
|
private replayEnabled;
|
|
115
|
+
private replaySampleRate;
|
|
54
116
|
private eventQueue;
|
|
55
117
|
private isProcessing;
|
|
56
118
|
private rrwebRecord;
|
|
@@ -67,6 +129,8 @@ declare class DataxamasTracking {
|
|
|
67
129
|
private recordingStartedAtMs;
|
|
68
130
|
private recordingSeq;
|
|
69
131
|
private recordingEvents;
|
|
132
|
+
private lastActivityAt;
|
|
133
|
+
private lastActivitySavedAt;
|
|
70
134
|
constructor(options: DataxamasTrackingOptions);
|
|
71
135
|
private isLocalhost;
|
|
72
136
|
private getSessionId;
|
|
@@ -89,7 +153,12 @@ declare class DataxamasTracking {
|
|
|
89
153
|
private isReplaySampled;
|
|
90
154
|
private isRecording;
|
|
91
155
|
private exceededReplayCaps;
|
|
156
|
+
private loadRecording;
|
|
157
|
+
private saveRecording;
|
|
92
158
|
private rotateRecording;
|
|
159
|
+
/** Reload ou navegação completa na mesma aba continua a gravação, se a visita ainda estiver ativa. */
|
|
160
|
+
private resumeOrRotateRecording;
|
|
161
|
+
private touchActivity;
|
|
93
162
|
private flushReplay;
|
|
94
163
|
/**
|
|
95
164
|
* Um full snapshot com CSS inline passa fácil de 100KB cru, e era isso que tomava 413 no
|
|
@@ -99,13 +168,14 @@ declare class DataxamasTracking {
|
|
|
99
168
|
private sendReplayBatch;
|
|
100
169
|
private stopReplayRecorder;
|
|
101
170
|
/**
|
|
102
|
-
* Bateu o teto: encerra a gravação e fica parado até a próxima
|
|
171
|
+
* Bateu o teto: encerra a gravação e fica parado até a próxima interação, em vez de emendar um
|
|
103
172
|
* clipe atrás do outro enquanto a página só anima sozinha.
|
|
104
173
|
*/
|
|
105
174
|
private scheduleReplayCutoff;
|
|
106
175
|
/**
|
|
107
|
-
* Primeiro gesto
|
|
108
|
-
*
|
|
176
|
+
* Primeiro gesto liga o gravador; os seguintes só o religam se ele tiver parado (aba que foi
|
|
177
|
+
* para segundo plano, reload), no mesmo `recordingId`. Depois do teto ou de muito tempo parado,
|
|
178
|
+
* o gesto abre uma gravação nova.
|
|
109
179
|
*/
|
|
110
180
|
private markReplayActivity;
|
|
111
181
|
private setupReplayLifecycle;
|
|
@@ -122,6 +192,7 @@ interface DataxamasOptions {
|
|
|
122
192
|
release?: string;
|
|
123
193
|
enableLogs?: boolean;
|
|
124
194
|
enableErrorTracking?: boolean;
|
|
195
|
+
enableCrawlerTracking?: boolean;
|
|
125
196
|
captureGlobalErrors?: boolean;
|
|
126
197
|
replayEnabled?: boolean;
|
|
127
198
|
heatMapEnabled?: boolean;
|
|
@@ -130,6 +201,7 @@ declare class Dataxamas {
|
|
|
130
201
|
readonly logs: DataxamasLogs;
|
|
131
202
|
readonly exceptions: DataxamasExceptions;
|
|
132
203
|
readonly tracking: DataxamasTracking;
|
|
204
|
+
readonly crawlers: DataxamasCrawlers;
|
|
133
205
|
constructor(options: DataxamasOptions);
|
|
134
206
|
}
|
|
135
207
|
|
|
@@ -139,4 +211,4 @@ declare class DataxamasApiError extends Error {
|
|
|
139
211
|
constructor(code: string, message: string, status: number);
|
|
140
212
|
}
|
|
141
213
|
|
|
142
|
-
export { Dataxamas, DataxamasApiError, DataxamasExceptions, DataxamasLogs, type DataxamasOptions, DataxamasTracking, type DataxamasTrackingOptions, type ErrorLevel, type ErrorSource, type ExceptionsConfig, type LogLevel, type LogsConfig };
|
|
214
|
+
export { type CrawlHit, type CrawlersConfig, Dataxamas, DataxamasApiError, DataxamasCrawlers, DataxamasExceptions, DataxamasLogs, type DataxamasOptions, DataxamasTracking, type DataxamasTrackingOptions, type ErrorLevel, type ErrorSource, type ExceptionsConfig, type LogLevel, type LogsConfig, type TrackCrawlOptions, createExpressCrawlerMiddleware, isCrawlerUserAgent, withCrawlerTracking };
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,116 @@
|
|
|
1
1
|
// src/constants.ts
|
|
2
|
-
var BASE_URL = "https://dataxamas.
|
|
2
|
+
var BASE_URL = "https://dataxamas.com";
|
|
3
3
|
|
|
4
4
|
// src/utils.ts
|
|
5
5
|
var isBrowser = () => typeof window !== "undefined" && typeof document !== "undefined";
|
|
6
6
|
|
|
7
|
+
// src/crawlers.ts
|
|
8
|
+
var CRAWLER_UA = /ChatGPT-User|GPTBot|OAI-SearchBot|Claude-User|Claude-SearchBot|ClaudeBot|Claude-Web|anthropic-ai|Perplexity|Gemini-Deep-Research|Googlebot|Google-InspectionTool|Storebot-Google|Google-CloudVertexBot|bingbot|BingPreview|adidxbot|MistralAI-User|DuckAssistBot|DuckDuckBot|meta-external|Applebot|Amazonbot|Bytespider|CCBot|cohere|YandexBot/i;
|
|
9
|
+
var CRAWLER_FILES = /\/(robots\.txt|llms(-full)?\.txt|sitemap[^/]*\.xml)$|\.md$/i;
|
|
10
|
+
var STATIC_ASSET = /\.(js|mjs|css|map|json|png|jpe?g|gif|webp|avif|svg|ico|woff2?|ttf|otf|eot|mp4|webm|mp3|wav|pdf|zip|txt|xml)$/i;
|
|
11
|
+
var IGNORED_PREFIXES = ["/_next/", "/assets/", "/static/", "/api/", "/_build/", "/@vite/", "/node_modules/"];
|
|
12
|
+
function isCrawlerUserAgent(userAgent) {
|
|
13
|
+
return Boolean(userAgent) && CRAWLER_UA.test(userAgent);
|
|
14
|
+
}
|
|
15
|
+
function shouldTrackPath(pathname) {
|
|
16
|
+
if (CRAWLER_FILES.test(pathname)) return true;
|
|
17
|
+
if (IGNORED_PREFIXES.some((prefix) => pathname.startsWith(prefix))) return false;
|
|
18
|
+
return !STATIC_ASSET.test(pathname);
|
|
19
|
+
}
|
|
20
|
+
function ipFromHeaders(get) {
|
|
21
|
+
const forwarded = get("x-forwarded-for");
|
|
22
|
+
if (forwarded) return forwarded.split(",")[0]?.trim() || null;
|
|
23
|
+
return get("cf-connecting-ip") ?? get("x-real-ip") ?? null;
|
|
24
|
+
}
|
|
25
|
+
var DataxamasCrawlers = class {
|
|
26
|
+
constructor(config) {
|
|
27
|
+
this.config = config;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Reports a fetch-style request if it came from a known crawler. Do not await it: call it and
|
|
31
|
+
* return your response. The report is fire-and-forget and swallows every network error.
|
|
32
|
+
*/
|
|
33
|
+
trackRequest(request, options = {}) {
|
|
34
|
+
const userAgent = request.headers.get("user-agent");
|
|
35
|
+
if (!userAgent) return;
|
|
36
|
+
const promise = this.track({
|
|
37
|
+
url: request.url,
|
|
38
|
+
userAgent,
|
|
39
|
+
ip: options.ip !== void 0 ? options.ip : ipFromHeaders((name) => request.headers.get(name)),
|
|
40
|
+
statusCode: options.statusCode
|
|
41
|
+
});
|
|
42
|
+
options.waitUntil?.(promise);
|
|
43
|
+
}
|
|
44
|
+
/** Lower-level variant for servers without a `Request` object. Resolves once sent; never rejects. */
|
|
45
|
+
track(hit) {
|
|
46
|
+
if (!this.config.enabled || isBrowser() || !isCrawlerUserAgent(hit.userAgent)) {
|
|
47
|
+
return Promise.resolve();
|
|
48
|
+
}
|
|
49
|
+
let url;
|
|
50
|
+
try {
|
|
51
|
+
url = new URL(hit.url);
|
|
52
|
+
} catch {
|
|
53
|
+
return Promise.resolve();
|
|
54
|
+
}
|
|
55
|
+
if (!shouldTrackPath(url.pathname)) return Promise.resolve();
|
|
56
|
+
url.search = "";
|
|
57
|
+
url.hash = "";
|
|
58
|
+
return fetch(`${this.config.baseUrl}/api/v1/crawls`, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: {
|
|
61
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
62
|
+
"Content-Type": "application/json"
|
|
63
|
+
},
|
|
64
|
+
body: JSON.stringify({
|
|
65
|
+
url: url.toString(),
|
|
66
|
+
userAgent: hit.userAgent,
|
|
67
|
+
ip: hit.ip ?? void 0,
|
|
68
|
+
statusCode: hit.statusCode
|
|
69
|
+
}),
|
|
70
|
+
signal: AbortSignal.timeout(this.config.timeoutMs ?? 2e3)
|
|
71
|
+
}).then(() => void 0).catch(() => void 0);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
function withCrawlerTracking(crawlers, handler) {
|
|
75
|
+
return async (request, ...args) => {
|
|
76
|
+
const response = await handler(request, ...args);
|
|
77
|
+
crawlers.trackRequest(request, { statusCode: response.status, waitUntil: findWaitUntil(args) });
|
|
78
|
+
return response;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function findWaitUntil(args) {
|
|
82
|
+
for (const arg of args) {
|
|
83
|
+
if (arg && typeof arg === "object" && "waitUntil" in arg && typeof arg.waitUntil === "function") {
|
|
84
|
+
const waitUntil = arg.waitUntil;
|
|
85
|
+
return (promise) => waitUntil.call(arg, promise);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return void 0;
|
|
89
|
+
}
|
|
90
|
+
function headerValue(headers, name) {
|
|
91
|
+
const value = headers[name];
|
|
92
|
+
return (Array.isArray(value) ? value[0] : value) ?? null;
|
|
93
|
+
}
|
|
94
|
+
function createExpressCrawlerMiddleware(crawlers) {
|
|
95
|
+
return (request, response, next) => {
|
|
96
|
+
const userAgent = headerValue(request.headers, "user-agent");
|
|
97
|
+
if (isCrawlerUserAgent(userAgent)) {
|
|
98
|
+
const host = headerValue(request.headers, "x-forwarded-host") ?? headerValue(request.headers, "host");
|
|
99
|
+
const protocol = request.protocol ?? "https";
|
|
100
|
+
const path = request.originalUrl ?? request.url ?? "/";
|
|
101
|
+
response.once("finish", () => {
|
|
102
|
+
void crawlers.track({
|
|
103
|
+
url: `${protocol}://${host}${path}`,
|
|
104
|
+
userAgent,
|
|
105
|
+
ip: request.ip ?? ipFromHeaders((name) => headerValue(request.headers, name)) ?? request.socket?.remoteAddress ?? null,
|
|
106
|
+
statusCode: response.statusCode
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
next();
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
7
114
|
// src/exceptions.ts
|
|
8
115
|
function normalizeError(error) {
|
|
9
116
|
if (error instanceof Error) {
|
|
@@ -211,19 +318,34 @@ var STORAGE_PREFIX = "@dataxamas";
|
|
|
211
318
|
var REPLAY_FLUSH_SIZE = 100;
|
|
212
319
|
var REPLAY_FLUSH_INTERVAL_MS = 5e3;
|
|
213
320
|
var CAP_CHECK_MS = 1e3;
|
|
214
|
-
var MAX_RECORDING_MS =
|
|
215
|
-
var MAX_EVENTS_PER_RECORDING =
|
|
321
|
+
var MAX_RECORDING_MS = 30 * 60 * 1e3;
|
|
322
|
+
var MAX_EVENTS_PER_RECORDING = 5e4;
|
|
323
|
+
var REPLAY_IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
324
|
+
var ACTIVITY_SAVE_THROTTLE_MS = 5e3;
|
|
325
|
+
var REPLAY_RECORDING_KEY = `${STORAGE_PREFIX}/replay-recording`;
|
|
326
|
+
function isStoredRecording(value) {
|
|
327
|
+
if (typeof value !== "object" || value === null) return false;
|
|
328
|
+
const stored = value;
|
|
329
|
+
return typeof stored.id === "string" && typeof stored.sessionId === "string" && typeof stored.startedAt === "string" && typeof stored.startedAtMs === "number" && typeof stored.seq === "number" && typeof stored.events === "number" && typeof stored.lastActivityAt === "number" && typeof stored.capped === "boolean";
|
|
330
|
+
}
|
|
216
331
|
var KEEPALIVE_LIMIT_BYTES = 60 * 1024;
|
|
217
|
-
var REPLAY_SAMPLE_RATE = 1;
|
|
218
332
|
var DataxamasTracking = class {
|
|
219
333
|
constructor(options) {
|
|
220
334
|
/**
|
|
221
|
-
* Primeiro gesto
|
|
222
|
-
*
|
|
335
|
+
* Primeiro gesto liga o gravador; os seguintes só o religam se ele tiver parado (aba que foi
|
|
336
|
+
* para segundo plano, reload), no mesmo `recordingId`. Depois do teto ou de muito tempo parado,
|
|
337
|
+
* o gesto abre uma gravação nova.
|
|
223
338
|
*/
|
|
224
339
|
this.markReplayActivity = () => {
|
|
225
|
-
if (this.replayHalted || this.
|
|
340
|
+
if (this.replayHalted || this.cutoffPending) return;
|
|
226
341
|
if (document.visibilityState === "hidden") return;
|
|
342
|
+
const now = Date.now();
|
|
343
|
+
if (this.replayCapped || now - this.lastActivityAt >= REPLAY_IDLE_TIMEOUT_MS) {
|
|
344
|
+
if (this.isRecording()) this.stopReplayRecorder(false);
|
|
345
|
+
this.rotateRecording();
|
|
346
|
+
}
|
|
347
|
+
this.touchActivity(now);
|
|
348
|
+
if (this.isRecording()) return;
|
|
227
349
|
this.startReplayRecorder();
|
|
228
350
|
};
|
|
229
351
|
if (!options.apiKey) {
|
|
@@ -236,6 +358,7 @@ var DataxamasTracking = class {
|
|
|
236
358
|
this.currentPage = isBrowser() ? window.location.pathname : "";
|
|
237
359
|
this.heatMapEnabled = options.heatMapEnabled ?? true;
|
|
238
360
|
this.replayEnabled = options.replayEnabled ?? true;
|
|
361
|
+
this.replaySampleRate = 1;
|
|
239
362
|
this.eventQueue = [];
|
|
240
363
|
this.isProcessing = false;
|
|
241
364
|
this.rrwebRecord = null;
|
|
@@ -252,6 +375,8 @@ var DataxamasTracking = class {
|
|
|
252
375
|
this.recordingStartedAtMs = 0;
|
|
253
376
|
this.recordingSeq = 0;
|
|
254
377
|
this.recordingEvents = 0;
|
|
378
|
+
this.lastActivityAt = 0;
|
|
379
|
+
this.lastActivitySavedAt = 0;
|
|
255
380
|
}
|
|
256
381
|
isLocalhost() {
|
|
257
382
|
return window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1";
|
|
@@ -313,8 +438,7 @@ var DataxamasTracking = class {
|
|
|
313
438
|
setInterval(() => {
|
|
314
439
|
const newPath = window.location.pathname;
|
|
315
440
|
if (newPath !== lastPath) {
|
|
316
|
-
this.
|
|
317
|
-
this.rotateRecording();
|
|
441
|
+
this.flushReplay(false);
|
|
318
442
|
this.currentPage = newPath;
|
|
319
443
|
this.sendEvent({ name: "PAGE_VIEW", path: newPath });
|
|
320
444
|
lastPath = newPath;
|
|
@@ -330,6 +454,9 @@ var DataxamasTracking = class {
|
|
|
330
454
|
const cfg = await res.json();
|
|
331
455
|
if (typeof cfg.heatMapEnabled === "boolean") this.heatMapEnabled = cfg.heatMapEnabled;
|
|
332
456
|
if (typeof cfg.replayEnabled === "boolean") this.replayEnabled = cfg.replayEnabled;
|
|
457
|
+
if (typeof cfg.replaySampleRate === "number" && cfg.replaySampleRate >= 0 && cfg.replaySampleRate <= 1) {
|
|
458
|
+
this.replaySampleRate = cfg.replaySampleRate;
|
|
459
|
+
}
|
|
333
460
|
} catch (_) {
|
|
334
461
|
}
|
|
335
462
|
}
|
|
@@ -423,17 +550,20 @@ var DataxamasTracking = class {
|
|
|
423
550
|
* de um replay picotado que não dá para assistir.
|
|
424
551
|
*/
|
|
425
552
|
isReplaySampled() {
|
|
426
|
-
|
|
427
|
-
if (
|
|
553
|
+
const rate = this.replaySampleRate;
|
|
554
|
+
if (rate >= 1) return true;
|
|
555
|
+
if (rate <= 0) return false;
|
|
556
|
+
const key = `${STORAGE_PREFIX}/replay-sample`;
|
|
428
557
|
try {
|
|
429
|
-
const
|
|
430
|
-
if (
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
558
|
+
const [storedRate, storedDecision] = (window.localStorage.getItem(key) ?? "").split(":");
|
|
559
|
+
if (Number(storedRate) === rate && (storedDecision === "1" || storedDecision === "0")) {
|
|
560
|
+
return storedDecision === "1";
|
|
561
|
+
}
|
|
562
|
+
const sampled = Math.random() < rate;
|
|
563
|
+
window.localStorage.setItem(key, `${rate}:${sampled ? "1" : "0"}`);
|
|
434
564
|
return sampled;
|
|
435
565
|
} catch (_) {
|
|
436
|
-
return Math.random() <
|
|
566
|
+
return Math.random() < rate;
|
|
437
567
|
}
|
|
438
568
|
}
|
|
439
569
|
isRecording() {
|
|
@@ -442,6 +572,31 @@ var DataxamasTracking = class {
|
|
|
442
572
|
exceededReplayCaps() {
|
|
443
573
|
return this.recordingEvents >= MAX_EVENTS_PER_RECORDING || Date.now() - this.recordingStartedAtMs >= MAX_RECORDING_MS;
|
|
444
574
|
}
|
|
575
|
+
loadRecording() {
|
|
576
|
+
try {
|
|
577
|
+
const parsed = JSON.parse(window.sessionStorage.getItem(REPLAY_RECORDING_KEY) ?? "null");
|
|
578
|
+
return isStoredRecording(parsed) ? parsed : null;
|
|
579
|
+
} catch (_) {
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
saveRecording() {
|
|
584
|
+
if (!this.sessionId || !this.recordingId || !this.recordingStartedAt) return;
|
|
585
|
+
const stored = {
|
|
586
|
+
id: this.recordingId,
|
|
587
|
+
sessionId: this.sessionId,
|
|
588
|
+
startedAt: this.recordingStartedAt,
|
|
589
|
+
startedAtMs: this.recordingStartedAtMs,
|
|
590
|
+
seq: this.recordingSeq,
|
|
591
|
+
events: this.recordingEvents,
|
|
592
|
+
lastActivityAt: this.lastActivityAt,
|
|
593
|
+
capped: this.replayCapped
|
|
594
|
+
};
|
|
595
|
+
try {
|
|
596
|
+
window.sessionStorage.setItem(REPLAY_RECORDING_KEY, JSON.stringify(stored));
|
|
597
|
+
} catch (_) {
|
|
598
|
+
}
|
|
599
|
+
}
|
|
445
600
|
rotateRecording() {
|
|
446
601
|
this.replayCapped = false;
|
|
447
602
|
this.recordingId = crypto.randomUUID();
|
|
@@ -449,6 +604,29 @@ var DataxamasTracking = class {
|
|
|
449
604
|
this.recordingStartedAtMs = Date.now();
|
|
450
605
|
this.recordingSeq = 0;
|
|
451
606
|
this.recordingEvents = 0;
|
|
607
|
+
this.lastActivityAt = Date.now();
|
|
608
|
+
this.saveRecording();
|
|
609
|
+
}
|
|
610
|
+
/** Reload ou navegação completa na mesma aba continua a gravação, se a visita ainda estiver ativa. */
|
|
611
|
+
resumeOrRotateRecording() {
|
|
612
|
+
const stored = this.loadRecording();
|
|
613
|
+
if (!stored || stored.capped || stored.sessionId !== this.sessionId || Date.now() - stored.lastActivityAt >= REPLAY_IDLE_TIMEOUT_MS) {
|
|
614
|
+
this.rotateRecording();
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
this.replayCapped = false;
|
|
618
|
+
this.recordingId = stored.id;
|
|
619
|
+
this.recordingStartedAt = stored.startedAt;
|
|
620
|
+
this.recordingStartedAtMs = stored.startedAtMs;
|
|
621
|
+
this.recordingSeq = stored.seq;
|
|
622
|
+
this.recordingEvents = stored.events;
|
|
623
|
+
this.lastActivityAt = stored.lastActivityAt;
|
|
624
|
+
}
|
|
625
|
+
touchActivity(now) {
|
|
626
|
+
this.lastActivityAt = now;
|
|
627
|
+
if (now - this.lastActivitySavedAt < ACTIVITY_SAVE_THROTTLE_MS) return;
|
|
628
|
+
this.lastActivitySavedAt = now;
|
|
629
|
+
this.saveRecording();
|
|
452
630
|
}
|
|
453
631
|
flushReplay(urgent) {
|
|
454
632
|
if (this.replayBuffer.length === 0 || !this.recordingId) return;
|
|
@@ -464,6 +642,7 @@ var DataxamasTracking = class {
|
|
|
464
642
|
...this.measureReplay(),
|
|
465
643
|
events
|
|
466
644
|
});
|
|
645
|
+
this.saveRecording();
|
|
467
646
|
void this.sendReplayBatch(payload, urgent);
|
|
468
647
|
}
|
|
469
648
|
/**
|
|
@@ -505,6 +684,8 @@ var DataxamasTracking = class {
|
|
|
505
684
|
const body = await res.json().catch(() => null);
|
|
506
685
|
if (body?.stop) {
|
|
507
686
|
this.replayHalted = true;
|
|
687
|
+
this.replayCapped = true;
|
|
688
|
+
this.saveRecording();
|
|
508
689
|
this.stopReplayRecorder(false);
|
|
509
690
|
}
|
|
510
691
|
} catch (_) {
|
|
@@ -518,13 +699,14 @@ var DataxamasTracking = class {
|
|
|
518
699
|
}
|
|
519
700
|
}
|
|
520
701
|
/**
|
|
521
|
-
* Bateu o teto: encerra a gravação e fica parado até a próxima
|
|
702
|
+
* Bateu o teto: encerra a gravação e fica parado até a próxima interação, em vez de emendar um
|
|
522
703
|
* clipe atrás do outro enquanto a página só anima sozinha.
|
|
523
704
|
*/
|
|
524
705
|
scheduleReplayCutoff() {
|
|
525
706
|
if (this.cutoffPending) return;
|
|
526
707
|
this.cutoffPending = true;
|
|
527
708
|
this.replayCapped = true;
|
|
709
|
+
this.saveRecording();
|
|
528
710
|
setTimeout(() => {
|
|
529
711
|
this.cutoffPending = false;
|
|
530
712
|
this.stopReplayRecorder(false);
|
|
@@ -601,6 +783,7 @@ var DataxamasTracking = class {
|
|
|
601
783
|
}
|
|
602
784
|
startTracking() {
|
|
603
785
|
this.sessionId = this.getSessionId() ?? this.createSessionId();
|
|
786
|
+
this.resumeOrRotateRecording();
|
|
604
787
|
this.sendEvent({ name: "PAGE_VIEW" });
|
|
605
788
|
this.trackPageChanges();
|
|
606
789
|
this.trackClicks();
|
|
@@ -615,6 +798,7 @@ var DataxamasTracking = class {
|
|
|
615
798
|
throw new Error("DataxamasTracking: This SDK can only be used in a browser environment.");
|
|
616
799
|
}
|
|
617
800
|
if (!this.allowLocalhost && this.isLocalhost()) return;
|
|
801
|
+
if (navigator.webdriver) return;
|
|
618
802
|
if (document.readyState === "loading") {
|
|
619
803
|
document.addEventListener("DOMContentLoaded", this.startTracking.bind(this));
|
|
620
804
|
} else {
|
|
@@ -651,6 +835,11 @@ var Dataxamas = class {
|
|
|
651
835
|
heatMapEnabled: options.heatMapEnabled ?? true,
|
|
652
836
|
allowLocalhost: options.allowLocalhost ?? false
|
|
653
837
|
});
|
|
838
|
+
this.crawlers = new DataxamasCrawlers({
|
|
839
|
+
apiKey: options.apiKey,
|
|
840
|
+
baseUrl,
|
|
841
|
+
enabled: options.enableCrawlerTracking ?? true
|
|
842
|
+
});
|
|
654
843
|
}
|
|
655
844
|
};
|
|
656
845
|
|
|
@@ -666,7 +855,11 @@ var DataxamasApiError = class extends Error {
|
|
|
666
855
|
export {
|
|
667
856
|
Dataxamas,
|
|
668
857
|
DataxamasApiError,
|
|
858
|
+
DataxamasCrawlers,
|
|
669
859
|
DataxamasExceptions,
|
|
670
860
|
DataxamasLogs,
|
|
671
|
-
DataxamasTracking
|
|
861
|
+
DataxamasTracking,
|
|
862
|
+
createExpressCrawlerMiddleware,
|
|
863
|
+
isCrawlerUserAgent,
|
|
864
|
+
withCrawlerTracking
|
|
672
865
|
};
|