@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 +4 -4
- package/dist/index.cjs +128 -12
- package/dist/index.d.cts +64 -1
- package/dist/index.d.ts +64 -1
- package/dist/index.js +118 -2
- 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}`);
|
|
@@ -358,10 +465,10 @@ var DataxamasTracking = class {
|
|
|
358
465
|
};
|
|
359
466
|
}
|
|
360
467
|
safeText(t) {
|
|
361
|
-
const tag = _optionalChain([t, 'access',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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
|
-
|
|
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.
|
|
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
|
};
|