@adwait12345/telemetry-core 0.1.2 → 0.1.4

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 ADDED
@@ -0,0 +1,127 @@
1
+ # `@adwait12345/telemetry-core`
2
+
3
+ The shared core of the Telemetry SDK. Contains bot detection logic, the send function with retry support, and all shared TypeScript types used by the framework adapters (`telemetry-next`, `telemetry-express`, `telemetry-astro`).
4
+
5
+ You typically do **not** install this directly — install the adapter for your framework instead. Use this package if you are building a custom adapter.
6
+
7
+ ---
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install @adwait12345/telemetry-core
13
+ # or
14
+ pnpm add @adwait12345/telemetry-core
15
+ ```
16
+
17
+ ---
18
+
19
+ ## What's included
20
+
21
+ ### `detectBot(req, customBots?)`
22
+
23
+ Runs multi-layer bot detection against a normalized request object.
24
+
25
+ **Detection layers (in priority order):**
26
+
27
+ | Layer | Method | Confidence |
28
+ |-------|--------|------------|
29
+ | 1 | Automation headers (`x-selenium`, `x-puppeteer`, `x-playwright`, etc.) | `certain` |
30
+ | 2 | HTTP/1.0 — no modern browser uses this | `high` |
31
+ | 3 | Named bot UA match (100+ known bots across 16 categories) | `certain` |
32
+ | 4 | Generic bot UA patterns (`/bot/i`, `/crawler/i`, empty/short UA, etc.) | `high` |
33
+ | 5 | Header anomaly — claims modern browser but missing `sec-fetch-site` / `accept-language` | `medium`–`high` |
34
+
35
+ ```typescript
36
+ import { detectBot } from "@adwait12345/telemetry-core";
37
+
38
+ const result = detectBot({
39
+ userAgent: "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)",
40
+ ip: "66.249.66.1",
41
+ path: "/",
42
+ method: "GET",
43
+ referrer: null,
44
+ acceptLanguage: null,
45
+ acceptEncoding: "gzip",
46
+ secFetchSite: null,
47
+ httpVersion: "1.1",
48
+ automationHeaders: [],
49
+ });
50
+
51
+ // result.isBot → true
52
+ // result.botName → "Googlebot"
53
+ // result.botCategory → "search"
54
+ // result.confidence → "certain"
55
+ // result.method → "ua-match"
56
+ ```
57
+
58
+ ### `extractAutomationHeaders(headers)`
59
+
60
+ Checks a plain headers object for known automation tool headers. Returns the list of matched header names. Call this before `detectBot` to populate `automationHeaders`.
61
+
62
+ ```typescript
63
+ import { extractAutomationHeaders } from "@adwait12345/telemetry-core";
64
+
65
+ const matched = extractAutomationHeaders(req.headers);
66
+ // e.g. ["x-playwright"] if Playwright is driving the browser
67
+ ```
68
+
69
+ ### `sendToTelemetry(payload, config)`
70
+
71
+ Sends a tracking payload to the Telemetry API. Includes:
72
+ - **3 attempts** with exponential backoff (200 ms → 400 ms)
73
+ - **5 second timeout** per attempt via `AbortController`
74
+ - **4xx errors skip retry** (they will not succeed on retry)
75
+ - Automatic `Authorization: Bearer {serverSecret}` header
76
+
77
+ ```typescript
78
+ import { sendToTelemetry } from "@adwait12345/telemetry-core";
79
+
80
+ await sendToTelemetry(payload, {
81
+ projectId: "your-project-id",
82
+ apiUrl: "https://telemetry-uqd3.onrender.com",
83
+ serverSecret: "sk_...",
84
+ });
85
+ ```
86
+
87
+ ---
88
+
89
+ ## Bot categories
90
+
91
+ The 100+ built-in bots are organized into 16 categories:
92
+
93
+ | Category | Examples |
94
+ |----------|---------|
95
+ | `ai-crawler` | GPTBot, ClaudeBot, PerplexityBot, Applebot |
96
+ | `ai-assistant` | ChatGPT-User, Claude-Web, Copilot |
97
+ | `search` | Googlebot, Bingbot, DuckDuckBot, Yandex |
98
+ | `seo` | AhrefsBot, SemrushBot, MajesticSEO |
99
+ | `advertising` | Mediapartners-Google, AdsBot-Google |
100
+ | `monitor` | UptimeRobot, Pingdom, StatusCake |
101
+ | `preview` | Slackbot, Twitterbot, facebookexternalhit |
102
+ | `webhook` | Stripe-Webhook, GitHub-Hookshot, Shopify |
103
+ | `feed` | Feedly, Inoreader, NewsBlur |
104
+ | `ecommerce` | Shopify, Wix |
105
+ | `verification` | Let's Encrypt, SSL Labs |
106
+ | `analytics` | New Relic, Datadog |
107
+ | `social` | LinkedInBot, Pinterest |
108
+ | `accessibility` | ChromeVox |
109
+ | `scraper` | Scrapy, HTTrack |
110
+ | `unknown` | Anything matching generic bot patterns |
111
+
112
+ ---
113
+
114
+ ## `TelemetryConfig` reference
115
+
116
+ | Option | Type | Default | Description |
117
+ |--------|------|---------|-------------|
118
+ | `projectId` | `string` | **required** | Your Telemetry project ID |
119
+ | `apiUrl` | `string` | hosted service | API base URL |
120
+ | `serverSecret` | `string` | — | Secret key for server-side auth (`Bearer {serverSecret}`) |
121
+ | `authorizationHeader` | `string` | — | Fully custom `Authorization` header value, overrides `serverSecret` |
122
+ | `headers` | `Record<string, string>` | — | Extra headers merged into every telemetry request |
123
+ | `trackAll` | `boolean` | `false` | Track all requests, not just bots |
124
+ | `trackSearchBots` | `boolean` | `true` | Include Googlebot, Bingbot etc. |
125
+ | `ignorePaths` | `(string \| RegExp)[]` | — | Paths to skip (exact strings or regex) |
126
+ | `customBots` | `Array<{name, pattern, category?}>` | — | Add your own bot definitions |
127
+ | `debug` | `boolean` | `false` | Enable verbose console logging |
package/dist/index.d.mts CHANGED
@@ -65,6 +65,18 @@ interface TelemetryConfig {
65
65
  * This is required since these requests originate from the server and cannot pass domain validation checks via Origin.
66
66
  */
67
67
  serverSecret?: string;
68
+ /**
69
+ * Fully custom Authorization header value.
70
+ * Overrides the default "Bearer {serverSecret}" format.
71
+ * e.g. "Token mytoken" or "Basic dXNlcjpwYXNz"
72
+ */
73
+ authorizationHeader?: string;
74
+ /**
75
+ * Additional custom headers to send with every telemetry request.
76
+ * These are merged on top of the defaults and can override them.
77
+ * e.g. { "x-workspace-id": "abc123" }
78
+ */
79
+ headers?: Record<string, string>;
68
80
  }
69
81
  /** Payload sent to the Telemetry server-side tracking endpoint */
70
82
  interface ServerTrackPayload {
@@ -142,7 +154,7 @@ declare const ALL_BOTS: BotDefinition[];
142
154
 
143
155
  /**
144
156
  * Sends a server-side payload to the Telemetry backend.
145
- * This is meant to be fire-and-forget, so it does not throw errors.
157
+ * Automatically retries on transient failures with exponential backoff.
146
158
  *
147
159
  * @param payload The data to track.
148
160
  * @param config The Telemetry configuration.
package/dist/index.d.ts CHANGED
@@ -65,6 +65,18 @@ interface TelemetryConfig {
65
65
  * This is required since these requests originate from the server and cannot pass domain validation checks via Origin.
66
66
  */
67
67
  serverSecret?: string;
68
+ /**
69
+ * Fully custom Authorization header value.
70
+ * Overrides the default "Bearer {serverSecret}" format.
71
+ * e.g. "Token mytoken" or "Basic dXNlcjpwYXNz"
72
+ */
73
+ authorizationHeader?: string;
74
+ /**
75
+ * Additional custom headers to send with every telemetry request.
76
+ * These are merged on top of the defaults and can override them.
77
+ * e.g. { "x-workspace-id": "abc123" }
78
+ */
79
+ headers?: Record<string, string>;
68
80
  }
69
81
  /** Payload sent to the Telemetry server-side tracking endpoint */
70
82
  interface ServerTrackPayload {
@@ -142,7 +154,7 @@ declare const ALL_BOTS: BotDefinition[];
142
154
 
143
155
  /**
144
156
  * Sends a server-side payload to the Telemetry backend.
145
- * This is meant to be fire-and-forget, so it does not throw errors.
157
+ * Automatically retries on transient failures with exponential backoff.
146
158
  *
147
159
  * @param payload The data to track.
148
160
  * @param config The Telemetry configuration.
package/dist/index.js CHANGED
@@ -1112,10 +1112,67 @@ function extractAutomationHeaders(headers) {
1112
1112
  }
1113
1113
 
1114
1114
  // src/index.ts
1115
+ function buildHeaders(config) {
1116
+ const headers = {
1117
+ "Content-Type": "application/json"
1118
+ };
1119
+ if (config.authorizationHeader) {
1120
+ headers["Authorization"] = config.authorizationHeader;
1121
+ } else if (config.serverSecret) {
1122
+ headers["Authorization"] = `Bearer ${config.serverSecret}`;
1123
+ }
1124
+ if (config.headers) {
1125
+ Object.assign(headers, config.headers);
1126
+ }
1127
+ return headers;
1128
+ }
1129
+ async function sendWithRetry(endpoint, payload, config, retries = 2) {
1130
+ const headers = buildHeaders(config);
1131
+ for (let attempt = 0; attempt <= retries; attempt++) {
1132
+ const controller = new AbortController();
1133
+ const timeout = setTimeout(() => controller.abort(), 5e3);
1134
+ try {
1135
+ const response = await fetch(endpoint, {
1136
+ method: "POST",
1137
+ headers,
1138
+ body: JSON.stringify(payload),
1139
+ signal: controller.signal
1140
+ });
1141
+ clearTimeout(timeout);
1142
+ if (response.ok) return;
1143
+ if (config.debug) {
1144
+ const text = await response.text();
1145
+ console.error(
1146
+ `[Telemetry] Attempt ${attempt + 1} failed: ${response.status} \u2014 ${text}`
1147
+ );
1148
+ }
1149
+ if (response.status >= 400 && response.status < 500) return;
1150
+ } catch (error) {
1151
+ clearTimeout(timeout);
1152
+ if (config.debug) {
1153
+ if (error?.name === "AbortError") {
1154
+ console.error(
1155
+ `[Telemetry] Attempt ${attempt + 1} timed out after 5s.`
1156
+ );
1157
+ } else {
1158
+ console.error(`[Telemetry] Attempt ${attempt + 1} error:`, error);
1159
+ }
1160
+ }
1161
+ }
1162
+ if (attempt < retries) {
1163
+ await new Promise((r) => setTimeout(r, 200 * Math.pow(2, attempt)));
1164
+ }
1165
+ }
1166
+ if (config.debug) {
1167
+ console.error("[Telemetry] All retry attempts failed \u2014 giving up.");
1168
+ }
1169
+ }
1115
1170
  async function sendToTelemetry(payload, config) {
1116
1171
  if (!config.apiUrl) {
1117
1172
  if (config.debug) {
1118
- console.warn("[Telemetry] No apiUrl configured \u2014 skipping telemetry send.");
1173
+ console.warn(
1174
+ "[Telemetry] No apiUrl configured \u2014 skipping telemetry send."
1175
+ );
1119
1176
  }
1120
1177
  return;
1121
1178
  }
@@ -1123,34 +1180,7 @@ async function sendToTelemetry(payload, config) {
1123
1180
  if (config.debug) {
1124
1181
  console.log(`[Telemetry] Sending payload to ${endpoint}`, payload);
1125
1182
  }
1126
- const controller = new AbortController();
1127
- const timeout = setTimeout(() => controller.abort(), 3e3);
1128
- try {
1129
- const response = await fetch(endpoint, {
1130
- method: "POST",
1131
- headers: {
1132
- "Content-Type": "application/json",
1133
- "Authorization": `Bearer ${config.serverSecret}`
1134
- },
1135
- body: JSON.stringify(payload),
1136
- signal: controller.signal
1137
- });
1138
- if (!response.ok && config.debug) {
1139
- console.error(`[Telemetry] Failed to send payload: ${response.status} ${response.statusText}`);
1140
- const text = await response.text();
1141
- console.error(`[Telemetry] Response body:`, text);
1142
- }
1143
- } catch (error) {
1144
- if (config.debug) {
1145
- if (error?.name === "AbortError") {
1146
- console.error("[Telemetry] Request timed out after 3s \u2014 skipping.");
1147
- } else {
1148
- console.error("[Telemetry] Error sending payload:", error);
1149
- }
1150
- }
1151
- } finally {
1152
- clearTimeout(timeout);
1153
- }
1183
+ await sendWithRetry(endpoint, payload, config);
1154
1184
  }
1155
1185
  // Annotate the CommonJS export names for ESM import in node:
1156
1186
  0 && (module.exports = {
package/dist/index.mjs CHANGED
@@ -1068,10 +1068,67 @@ function extractAutomationHeaders(headers) {
1068
1068
  }
1069
1069
 
1070
1070
  // src/index.ts
1071
+ function buildHeaders(config) {
1072
+ const headers = {
1073
+ "Content-Type": "application/json"
1074
+ };
1075
+ if (config.authorizationHeader) {
1076
+ headers["Authorization"] = config.authorizationHeader;
1077
+ } else if (config.serverSecret) {
1078
+ headers["Authorization"] = `Bearer ${config.serverSecret}`;
1079
+ }
1080
+ if (config.headers) {
1081
+ Object.assign(headers, config.headers);
1082
+ }
1083
+ return headers;
1084
+ }
1085
+ async function sendWithRetry(endpoint, payload, config, retries = 2) {
1086
+ const headers = buildHeaders(config);
1087
+ for (let attempt = 0; attempt <= retries; attempt++) {
1088
+ const controller = new AbortController();
1089
+ const timeout = setTimeout(() => controller.abort(), 5e3);
1090
+ try {
1091
+ const response = await fetch(endpoint, {
1092
+ method: "POST",
1093
+ headers,
1094
+ body: JSON.stringify(payload),
1095
+ signal: controller.signal
1096
+ });
1097
+ clearTimeout(timeout);
1098
+ if (response.ok) return;
1099
+ if (config.debug) {
1100
+ const text = await response.text();
1101
+ console.error(
1102
+ `[Telemetry] Attempt ${attempt + 1} failed: ${response.status} \u2014 ${text}`
1103
+ );
1104
+ }
1105
+ if (response.status >= 400 && response.status < 500) return;
1106
+ } catch (error) {
1107
+ clearTimeout(timeout);
1108
+ if (config.debug) {
1109
+ if (error?.name === "AbortError") {
1110
+ console.error(
1111
+ `[Telemetry] Attempt ${attempt + 1} timed out after 5s.`
1112
+ );
1113
+ } else {
1114
+ console.error(`[Telemetry] Attempt ${attempt + 1} error:`, error);
1115
+ }
1116
+ }
1117
+ }
1118
+ if (attempt < retries) {
1119
+ await new Promise((r) => setTimeout(r, 200 * Math.pow(2, attempt)));
1120
+ }
1121
+ }
1122
+ if (config.debug) {
1123
+ console.error("[Telemetry] All retry attempts failed \u2014 giving up.");
1124
+ }
1125
+ }
1071
1126
  async function sendToTelemetry(payload, config) {
1072
1127
  if (!config.apiUrl) {
1073
1128
  if (config.debug) {
1074
- console.warn("[Telemetry] No apiUrl configured \u2014 skipping telemetry send.");
1129
+ console.warn(
1130
+ "[Telemetry] No apiUrl configured \u2014 skipping telemetry send."
1131
+ );
1075
1132
  }
1076
1133
  return;
1077
1134
  }
@@ -1079,34 +1136,7 @@ async function sendToTelemetry(payload, config) {
1079
1136
  if (config.debug) {
1080
1137
  console.log(`[Telemetry] Sending payload to ${endpoint}`, payload);
1081
1138
  }
1082
- const controller = new AbortController();
1083
- const timeout = setTimeout(() => controller.abort(), 3e3);
1084
- try {
1085
- const response = await fetch(endpoint, {
1086
- method: "POST",
1087
- headers: {
1088
- "Content-Type": "application/json",
1089
- "Authorization": `Bearer ${config.serverSecret}`
1090
- },
1091
- body: JSON.stringify(payload),
1092
- signal: controller.signal
1093
- });
1094
- if (!response.ok && config.debug) {
1095
- console.error(`[Telemetry] Failed to send payload: ${response.status} ${response.statusText}`);
1096
- const text = await response.text();
1097
- console.error(`[Telemetry] Response body:`, text);
1098
- }
1099
- } catch (error) {
1100
- if (config.debug) {
1101
- if (error?.name === "AbortError") {
1102
- console.error("[Telemetry] Request timed out after 3s \u2014 skipping.");
1103
- } else {
1104
- console.error("[Telemetry] Error sending payload:", error);
1105
- }
1106
- }
1107
- } finally {
1108
- clearTimeout(timeout);
1109
- }
1139
+ await sendWithRetry(endpoint, payload, config);
1110
1140
  }
1111
1141
  export {
1112
1142
  ACCESSIBILITY_BOTS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adwait12345/telemetry-core",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Framework-agnostic core for Telemetry SDK — bot detection and server-side tracking",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -13,7 +13,8 @@
13
13
  }
14
14
  },
15
15
  "files": [
16
- "dist"
16
+ "dist",
17
+ "README.md"
17
18
  ],
18
19
  "devDependencies": {
19
20
  "tsup": "^8.0.0",