@dataxamas/tracker-js 1.0.1 → 2.1.0

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}`);
@@ -358,10 +465,10 @@ var DataxamasTracking = class {
358
465
  };
359
466
  }
360
467
  safeText(t) {
361
- const tag = _optionalChain([t, 'access', _6 => _6.tagName, 'optionalAccess', _7 => _7.toLowerCase, 'call', _8 => _8()]);
468
+ const tag = _optionalChain([t, 'access', _15 => _15.tagName, 'optionalAccess', _16 => _16.toLowerCase, 'call', _17 => _17()]);
362
469
  if (tag === "input" || tag === "textarea" || tag === "select" || tag === "option") return void 0;
363
470
  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)]);
471
+ return _optionalChain([t, 'access', _18 => _18.textContent, 'optionalAccess', _19 => _19.trim, 'call', _20 => _20(), 'access', _21 => _21.substring, 'call', _22 => _22(0, 50)]);
365
472
  }
366
473
  safeString(value, max) {
367
474
  if (typeof value !== "string") return void 0;
@@ -382,11 +489,11 @@ var DataxamasTracking = class {
382
489
  this.sendEvent({
383
490
  name: "CLICK",
384
491
  data: {
385
- tagName: _optionalChain([t, 'access', _14 => _14.tagName, 'optionalAccess', _15 => _15.toLowerCase, 'call', _16 => _16()]),
492
+ tagName: _optionalChain([t, 'access', _23 => _23.tagName, 'optionalAccess', _24 => _24.toLowerCase, 'call', _25 => _25()]),
386
493
  text: this.safeText(t),
387
494
  href: this.safeString(t.href, 2048),
388
495
  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),
496
+ classes: this.safeString(typeof t.className === "string" ? t.className : _optionalChain([t, 'access', _26 => _26.getAttribute, 'optionalCall', _27 => _27("class")]), 256),
390
497
  heatMap: this.buildHeatMap(e)
391
498
  }
392
499
  });
@@ -503,7 +610,7 @@ var DataxamasTracking = class {
503
610
  });
504
611
  if (!res.ok) return;
505
612
  const body = await res.json().catch(() => null);
506
- if (_optionalChain([body, 'optionalAccess', _19 => _19.stop])) {
613
+ if (_optionalChain([body, 'optionalAccess', _28 => _28.stop])) {
507
614
  this.replayHalted = true;
508
615
  this.stopReplayRecorder(false);
509
616
  }
@@ -651,6 +758,11 @@ var Dataxamas = class {
651
758
  heatMapEnabled: _nullishCoalesce(options.heatMapEnabled, () => ( true)),
652
759
  allowLocalhost: _nullishCoalesce(options.allowLocalhost, () => ( false))
653
760
  });
761
+ this.crawlers = new DataxamasCrawlers({
762
+ apiKey: options.apiKey,
763
+ baseUrl,
764
+ enabled: _nullishCoalesce(options.enableCrawlerTracking, () => ( true))
765
+ });
654
766
  }
655
767
  };
656
768
 
@@ -669,4 +781,8 @@ var DataxamasApiError = class extends Error {
669
781
 
670
782
 
671
783
 
672
- exports.Dataxamas = Dataxamas; exports.DataxamasApiError = DataxamasApiError; exports.DataxamasExceptions = DataxamasExceptions; exports.DataxamasLogs = DataxamasLogs; exports.DataxamasTracking = DataxamasTracking;
784
+
785
+
786
+
787
+
788
+ 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 {
@@ -122,6 +183,7 @@ interface DataxamasOptions {
122
183
  release?: string;
123
184
  enableLogs?: boolean;
124
185
  enableErrorTracking?: boolean;
186
+ enableCrawlerTracking?: boolean;
125
187
  captureGlobalErrors?: boolean;
126
188
  replayEnabled?: boolean;
127
189
  heatMapEnabled?: boolean;
@@ -130,6 +192,7 @@ declare class Dataxamas {
130
192
  readonly logs: DataxamasLogs;
131
193
  readonly exceptions: DataxamasExceptions;
132
194
  readonly tracking: DataxamasTracking;
195
+ readonly crawlers: DataxamasCrawlers;
133
196
  constructor(options: DataxamasOptions);
134
197
  }
135
198
 
@@ -139,4 +202,4 @@ declare class DataxamasApiError extends Error {
139
202
  constructor(code: string, message: string, status: number);
140
203
  }
141
204
 
142
- export { Dataxamas, DataxamasApiError, DataxamasExceptions, DataxamasLogs, type DataxamasOptions, DataxamasTracking, type DataxamasTrackingOptions, type ErrorLevel, type ErrorSource, type ExceptionsConfig, type LogLevel, type LogsConfig };
205
+ 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 {
@@ -122,6 +183,7 @@ interface DataxamasOptions {
122
183
  release?: string;
123
184
  enableLogs?: boolean;
124
185
  enableErrorTracking?: boolean;
186
+ enableCrawlerTracking?: boolean;
125
187
  captureGlobalErrors?: boolean;
126
188
  replayEnabled?: boolean;
127
189
  heatMapEnabled?: boolean;
@@ -130,6 +192,7 @@ declare class Dataxamas {
130
192
  readonly logs: DataxamasLogs;
131
193
  readonly exceptions: DataxamasExceptions;
132
194
  readonly tracking: DataxamasTracking;
195
+ readonly crawlers: DataxamasCrawlers;
133
196
  constructor(options: DataxamasOptions);
134
197
  }
135
198
 
@@ -139,4 +202,4 @@ declare class DataxamasApiError extends Error {
139
202
  constructor(code: string, message: string, status: number);
140
203
  }
141
204
 
142
- export { Dataxamas, DataxamasApiError, DataxamasExceptions, DataxamasLogs, type DataxamasOptions, DataxamasTracking, type DataxamasTrackingOptions, type ErrorLevel, type ErrorSource, type ExceptionsConfig, type LogLevel, type LogsConfig };
205
+ 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) {
@@ -651,6 +758,11 @@ var Dataxamas = class {
651
758
  heatMapEnabled: options.heatMapEnabled ?? true,
652
759
  allowLocalhost: options.allowLocalhost ?? false
653
760
  });
761
+ this.crawlers = new DataxamasCrawlers({
762
+ apiKey: options.apiKey,
763
+ baseUrl,
764
+ enabled: options.enableCrawlerTracking ?? true
765
+ });
654
766
  }
655
767
  };
656
768
 
@@ -666,7 +778,11 @@ var DataxamasApiError = class extends Error {
666
778
  export {
667
779
  Dataxamas,
668
780
  DataxamasApiError,
781
+ DataxamasCrawlers,
669
782
  DataxamasExceptions,
670
783
  DataxamasLogs,
671
- DataxamasTracking
784
+ DataxamasTracking,
785
+ createExpressCrawlerMiddleware,
786
+ isCrawlerUserAgent,
787
+ withCrawlerTracking
672
788
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dataxamas/tracker-js",
3
- "version": "1.0.1",
3
+ "version": "2.1.0",
4
4
  "type": "module",
5
5
  "description": "Node.js SDK for Dataxamas Analytics API",
6
6
  "license": "MIT",