@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # Dataxamas SDK
2
2
 
3
- Official JavaScript/TypeScript SDK for the [Dataxamas](https://dataxamas.izakdvlpr.com) analytics platform.
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.izakdvlpr.com` | API base URL. |
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.izakdvlpr.com` | API base URL. |
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.izakdvlpr.com`. Override with `baseUrl`.
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.izakdvlpr.com";
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 (e2) {
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', _2 => _2.error, 'optionalAccess', _3 => _3.message]), () => ( `Request failed with status ${response.status}`));
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 (e3) {
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 (e4) {
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', _4 => _4.error, 'optionalAccess', _5 => _5.message]), () => ( `Request failed with status ${response.status}`));
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 = 10 * 60 * 1e3;
215
- var MAX_EVENTS_PER_RECORDING = 2e4;
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 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`.
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.replayCapped || this.isRecording()) return;
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.stopReplayRecorder(false);
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', _6 => _6.tagName, 'optionalAccess', _7 => _7.toLowerCase, 'call', _8 => _8()]);
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', _9 => _9.textContent, 'optionalAccess', _10 => _10.trim, 'call', _11 => _11(), 'access', _12 => _12.substring, 'call', _13 => _13(0, 50)]);
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', _14 => _14.tagName, 'optionalAccess', _15 => _15.toLowerCase, 'call', _16 => _16()]),
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', _17 => _17.getAttribute, 'optionalCall', _18 => _18("class")]), 256),
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
- if (REPLAY_SAMPLE_RATE >= 1) return true;
427
- if (REPLAY_SAMPLE_RATE <= 0) return false;
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 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");
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() < REPLAY_SAMPLE_RATE;
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', _19 => _19.stop])) {
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 pageview, em vez de emendar um
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
- exports.Dataxamas = Dataxamas; exports.DataxamasApiError = DataxamasApiError; exports.DataxamasExceptions = DataxamasExceptions; exports.DataxamasLogs = DataxamasLogs; exports.DataxamasTracking = DataxamasTracking;
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 pageview, em vez de emendar um
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 da pageview liga o gravador; os seguintes só o religam se ele tiver parado
108
- * (aba que foi para segundo plano, por exemplo), sempre no mesmo `recordingId`.
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 pageview, em vez de emendar um
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 da pageview liga o gravador; os seguintes só o religam se ele tiver parado
108
- * (aba que foi para segundo plano, por exemplo), sempre no mesmo `recordingId`.
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.izakdvlpr.com";
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 = 10 * 60 * 1e3;
215
- var MAX_EVENTS_PER_RECORDING = 2e4;
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 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`.
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.replayCapped || this.isRecording()) return;
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.stopReplayRecorder(false);
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
- if (REPLAY_SAMPLE_RATE >= 1) return true;
427
- if (REPLAY_SAMPLE_RATE <= 0) return false;
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 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");
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() < REPLAY_SAMPLE_RATE;
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 pageview, em vez de emendar um
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dataxamas/tracker-js",
3
- "version": "2.0.1",
3
+ "version": "2.1.2",
4
4
  "type": "module",
5
5
  "description": "Node.js SDK for Dataxamas Analytics API",
6
6
  "license": "MIT",