@zaplier/sdk 1.0.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/LICENSE +21 -0
- package/README.md +144 -0
- package/dist/index.cjs +11092 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +1204 -0
- package/dist/index.esm.js +11059 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +3517 -0
- package/dist/index.js.map +1 -0
- package/dist/sdk.js +11098 -0
- package/dist/sdk.js.map +1 -0
- package/dist/sdk.min.js +7 -0
- package/dist/src/index.d.ts +15 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/modules/anti-adblock.d.ts +108 -0
- package/dist/src/modules/anti-adblock.d.ts.map +1 -0
- package/dist/src/modules/bot-detection.d.ts +15 -0
- package/dist/src/modules/bot-detection.d.ts.map +1 -0
- package/dist/src/modules/fingerprint/accessibility.d.ts +155 -0
- package/dist/src/modules/fingerprint/accessibility.d.ts.map +1 -0
- package/dist/src/modules/fingerprint/audio.d.ts +16 -0
- package/dist/src/modules/fingerprint/audio.d.ts.map +1 -0
- package/dist/src/modules/fingerprint/browser-apis.d.ts +108 -0
- package/dist/src/modules/fingerprint/browser-apis.d.ts.map +1 -0
- package/dist/src/modules/fingerprint/browser.d.ts +14 -0
- package/dist/src/modules/fingerprint/browser.d.ts.map +1 -0
- package/dist/src/modules/fingerprint/canvas.d.ts +14 -0
- package/dist/src/modules/fingerprint/canvas.d.ts.map +1 -0
- package/dist/src/modules/fingerprint/confidence.d.ts +89 -0
- package/dist/src/modules/fingerprint/confidence.d.ts.map +1 -0
- package/dist/src/modules/fingerprint/datetime-locale.d.ts +76 -0
- package/dist/src/modules/fingerprint/datetime-locale.d.ts.map +1 -0
- package/dist/src/modules/fingerprint/device-signals.d.ts +29 -0
- package/dist/src/modules/fingerprint/device-signals.d.ts.map +1 -0
- package/dist/src/modules/fingerprint/dom-blockers.d.ts +56 -0
- package/dist/src/modules/fingerprint/dom-blockers.d.ts.map +1 -0
- package/dist/src/modules/fingerprint/font-preferences.d.ts +55 -0
- package/dist/src/modules/fingerprint/font-preferences.d.ts.map +1 -0
- package/dist/src/modules/fingerprint/fonts-enhanced.d.ts +43 -0
- package/dist/src/modules/fingerprint/fonts-enhanced.d.ts.map +1 -0
- package/dist/src/modules/fingerprint/fonts.d.ts +14 -0
- package/dist/src/modules/fingerprint/fonts.d.ts.map +1 -0
- package/dist/src/modules/fingerprint/hardware.d.ts +40 -0
- package/dist/src/modules/fingerprint/hardware.d.ts.map +1 -0
- package/dist/src/modules/fingerprint/hashing.d.ts +28 -0
- package/dist/src/modules/fingerprint/hashing.d.ts.map +1 -0
- package/dist/src/modules/fingerprint/incognito.d.ts +6 -0
- package/dist/src/modules/fingerprint/incognito.d.ts.map +1 -0
- package/dist/src/modules/fingerprint/math-enhanced.d.ts +70 -0
- package/dist/src/modules/fingerprint/math-enhanced.d.ts.map +1 -0
- package/dist/src/modules/fingerprint/math.d.ts +32 -0
- package/dist/src/modules/fingerprint/math.d.ts.map +1 -0
- package/dist/src/modules/fingerprint/plugins-enhanced.d.ts +97 -0
- package/dist/src/modules/fingerprint/plugins-enhanced.d.ts.map +1 -0
- package/dist/src/modules/fingerprint/screen.d.ts +15 -0
- package/dist/src/modules/fingerprint/screen.d.ts.map +1 -0
- package/dist/src/modules/fingerprint/storage.d.ts +45 -0
- package/dist/src/modules/fingerprint/storage.d.ts.map +1 -0
- package/dist/src/modules/fingerprint/system.d.ts +40 -0
- package/dist/src/modules/fingerprint/system.d.ts.map +1 -0
- package/dist/src/modules/fingerprint/webgl.d.ts +15 -0
- package/dist/src/modules/fingerprint/webgl.d.ts.map +1 -0
- package/dist/src/modules/fingerprint.d.ts +35 -0
- package/dist/src/modules/fingerprint.d.ts.map +1 -0
- package/dist/src/modules/global-interface.d.ts +141 -0
- package/dist/src/modules/global-interface.d.ts.map +1 -0
- package/dist/src/modules/heatmap.d.ts +140 -0
- package/dist/src/modules/heatmap.d.ts.map +1 -0
- package/dist/src/modules/incognito-detection.d.ts +23 -0
- package/dist/src/modules/incognito-detection.d.ts.map +1 -0
- package/dist/src/modules/session-replay.d.ts +132 -0
- package/dist/src/modules/session-replay.d.ts.map +1 -0
- package/dist/src/modules/user-agent.d.ts +35 -0
- package/dist/src/modules/user-agent.d.ts.map +1 -0
- package/dist/src/sdk.d.ts +227 -0
- package/dist/src/sdk.d.ts.map +1 -0
- package/dist/src/types/config.d.ts +44 -0
- package/dist/src/types/config.d.ts.map +1 -0
- package/dist/src/types/detection.d.ts +114 -0
- package/dist/src/types/detection.d.ts.map +1 -0
- package/dist/src/types/events.d.ts +174 -0
- package/dist/src/types/events.d.ts.map +1 -0
- package/dist/src/types/fingerprint.d.ts +157 -0
- package/dist/src/types/fingerprint.d.ts.map +1 -0
- package/dist/src/types/index.d.ts +83 -0
- package/dist/src/types/index.d.ts.map +1 -0
- package/dist/src/types/methods.d.ts +83 -0
- package/dist/src/types/methods.d.ts.map +1 -0
- package/dist/src/types/visitor.d.ts +90 -0
- package/dist/src/types/visitor.d.ts.map +1 -0
- package/dist/src/utils/browser.d.ts +79 -0
- package/dist/src/utils/browser.d.ts.map +1 -0
- package/dist/src/utils/lazy-loader.d.ts +60 -0
- package/dist/src/utils/lazy-loader.d.ts.map +1 -0
- package/dist/src/utils/webgl-cache.d.ts +43 -0
- package/dist/src/utils/webgl-cache.d.ts.map +1 -0
- package/package.json +82 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3517 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* RabbitTracker SDK v3.0.0
|
|
3
|
+
* Advanced privacy-first tracking with fingerprinting and security detection
|
|
4
|
+
* (c) 2025 RabbitTracker Team
|
|
5
|
+
* Released under the MIT License
|
|
6
|
+
* https://rabbitracker.com
|
|
7
|
+
*/
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* MurmurHash3 x64 Implementation
|
|
14
|
+
* Based on FingerprintJS hashing utility for consistent fingerprint generation
|
|
15
|
+
*
|
|
16
|
+
* Provides 128-bit hash output using the x64 variant of MurmurHash3
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Adds two 64-bit numbers
|
|
20
|
+
*/
|
|
21
|
+
function x64Add(a, b) {
|
|
22
|
+
const low = (a[1] + b[1]) & 0xffffffff;
|
|
23
|
+
const high = (a[0] + b[0] + (low < a[1] ? 1 : 0)) & 0xffffffff;
|
|
24
|
+
return [high, low];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Multiplies two 64-bit numbers
|
|
28
|
+
*/
|
|
29
|
+
function x64Multiply(a, b) {
|
|
30
|
+
const al = a[1] & 0xffff;
|
|
31
|
+
const ah = a[1] >>> 16;
|
|
32
|
+
const bl = b[1] & 0xffff;
|
|
33
|
+
const bh = b[1] >>> 16;
|
|
34
|
+
const rl = al * bl;
|
|
35
|
+
const rm = (al * bh + ah * bl) & 0xffffffff;
|
|
36
|
+
const rh = ah * bh + (rm >>> 16);
|
|
37
|
+
const low = (rl + ((rm & 0xffff) << 16)) & 0xffffffff;
|
|
38
|
+
const high = (a[0] * b[1] + a[1] * b[0] + rh) & 0xffffffff;
|
|
39
|
+
return [high, low];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Rotates a 64-bit number left by the specified number of bits
|
|
43
|
+
*/
|
|
44
|
+
function x64Rotl(value, shift) {
|
|
45
|
+
shift %= 64;
|
|
46
|
+
if (shift === 0)
|
|
47
|
+
return value;
|
|
48
|
+
if (shift < 32) {
|
|
49
|
+
const high = (value[0] << shift) | (value[1] >>> (32 - shift));
|
|
50
|
+
const low = (value[1] << shift) | (value[0] >>> (32 - shift));
|
|
51
|
+
return [high & 0xffffffff, low & 0xffffffff];
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
shift -= 32;
|
|
55
|
+
const high = (value[1] << shift) | (value[0] >>> (32 - shift));
|
|
56
|
+
const low = (value[0] << shift) | (value[1] >>> (32 - shift));
|
|
57
|
+
return [high & 0xffffffff, low & 0xffffffff];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* XORs two 64-bit numbers
|
|
62
|
+
*/
|
|
63
|
+
function x64Xor(a, b) {
|
|
64
|
+
return [a[0] ^ b[0], a[1] ^ b[1]];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Finalizes a 64-bit hash value
|
|
68
|
+
*/
|
|
69
|
+
function x64Fmix(hash) {
|
|
70
|
+
let h = hash;
|
|
71
|
+
h = x64Xor(h, [0, h[0] >>> 1]);
|
|
72
|
+
h = x64Multiply(h, [0xff51afd7, 0xed558ccd]);
|
|
73
|
+
h = x64Xor(h, [0, h[0] >>> 1]);
|
|
74
|
+
h = x64Multiply(h, [0xc4ceb9fe, 0x1a85ec53]);
|
|
75
|
+
h = x64Xor(h, [0, h[0] >>> 1]);
|
|
76
|
+
return h;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Converts a string to array of 32-bit integers (little-endian)
|
|
80
|
+
*/
|
|
81
|
+
function stringToUint32Array(str) {
|
|
82
|
+
const arr = [];
|
|
83
|
+
for (let i = 0; i < str.length; i += 4) {
|
|
84
|
+
let value = 0;
|
|
85
|
+
for (let j = Math.min(3, str.length - i - 1); j >= 0; j--) {
|
|
86
|
+
value = (value << 8) | str.charCodeAt(i + j);
|
|
87
|
+
}
|
|
88
|
+
arr.push(value);
|
|
89
|
+
}
|
|
90
|
+
return arr;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* MurmurHash3 x64 128-bit hash function
|
|
94
|
+
* Returns a 128-bit hash as a hex string
|
|
95
|
+
*/
|
|
96
|
+
function x64hash128(input, seed = 0) {
|
|
97
|
+
const data = stringToUint32Array(input);
|
|
98
|
+
const nblocks = Math.floor(data.length / 4);
|
|
99
|
+
let h1 = [0, seed];
|
|
100
|
+
let h2 = [0, seed];
|
|
101
|
+
const c1 = [0x87c37b91, 0x114253d5];
|
|
102
|
+
const c2 = [0x4cf5ad43, 0x2745937f];
|
|
103
|
+
// Process 16-byte blocks
|
|
104
|
+
for (let i = 0; i < nblocks; i++) {
|
|
105
|
+
const k1Index = i * 4;
|
|
106
|
+
const k2Index = i * 4 + 2;
|
|
107
|
+
let k1 = [data[k1Index + 1] || 0, data[k1Index] || 0];
|
|
108
|
+
let k2 = [data[k2Index + 1] || 0, data[k2Index] || 0];
|
|
109
|
+
k1 = x64Multiply(k1, c1);
|
|
110
|
+
k1 = x64Rotl(k1, 31);
|
|
111
|
+
k1 = x64Multiply(k1, c2);
|
|
112
|
+
h1 = x64Xor(h1, k1);
|
|
113
|
+
h1 = x64Rotl(h1, 27);
|
|
114
|
+
h1 = x64Add(h1, h2);
|
|
115
|
+
h1 = x64Add(x64Multiply(h1, [0, 5]), [0, 0x52dce729]);
|
|
116
|
+
k2 = x64Multiply(k2, c2);
|
|
117
|
+
k2 = x64Rotl(k2, 33);
|
|
118
|
+
k2 = x64Multiply(k2, c1);
|
|
119
|
+
h2 = x64Xor(h2, k2);
|
|
120
|
+
h2 = x64Rotl(h2, 31);
|
|
121
|
+
h2 = x64Add(h2, h1);
|
|
122
|
+
h2 = x64Add(x64Multiply(h2, [0, 5]), [0, 0x38495ab5]);
|
|
123
|
+
}
|
|
124
|
+
// Process remaining bytes
|
|
125
|
+
const tailIndex = nblocks * 4;
|
|
126
|
+
const remainingBytes = data.length - tailIndex;
|
|
127
|
+
let k1 = [0, 0];
|
|
128
|
+
let k2 = [0, 0];
|
|
129
|
+
if (remainingBytes >= 12) {
|
|
130
|
+
k2 = x64Xor(k2, [data[tailIndex + 3] || 0, 0]);
|
|
131
|
+
}
|
|
132
|
+
if (remainingBytes >= 8) {
|
|
133
|
+
k2 = x64Xor(k2, [0, data[tailIndex + 2] || 0]);
|
|
134
|
+
k2 = x64Multiply(k2, c2);
|
|
135
|
+
k2 = x64Rotl(k2, 33);
|
|
136
|
+
k2 = x64Multiply(k2, c1);
|
|
137
|
+
h2 = x64Xor(h2, k2);
|
|
138
|
+
}
|
|
139
|
+
if (remainingBytes >= 4) {
|
|
140
|
+
k1 = x64Xor(k1, [data[tailIndex + 1] || 0, 0]);
|
|
141
|
+
}
|
|
142
|
+
if (remainingBytes >= 1) {
|
|
143
|
+
k1 = x64Xor(k1, [0, data[tailIndex] || 0]);
|
|
144
|
+
k1 = x64Multiply(k1, c1);
|
|
145
|
+
k1 = x64Rotl(k1, 31);
|
|
146
|
+
k1 = x64Multiply(k1, c2);
|
|
147
|
+
h1 = x64Xor(h1, k1);
|
|
148
|
+
}
|
|
149
|
+
// Finalization
|
|
150
|
+
h1 = x64Xor(h1, [0, input.length]);
|
|
151
|
+
h2 = x64Xor(h2, [0, input.length]);
|
|
152
|
+
h1 = x64Add(h1, h2);
|
|
153
|
+
h2 = x64Add(h2, h1);
|
|
154
|
+
h1 = x64Fmix(h1);
|
|
155
|
+
h2 = x64Fmix(h2);
|
|
156
|
+
h1 = x64Add(h1, h2);
|
|
157
|
+
h2 = x64Add(h2, h1);
|
|
158
|
+
// Convert to hex string
|
|
159
|
+
const hex1 = h1[0].toString(16).padStart(8, '0') + h1[1].toString(16).padStart(8, '0');
|
|
160
|
+
const hex2 = h2[0].toString(16).padStart(8, '0') + h2[1].toString(16).padStart(8, '0');
|
|
161
|
+
return hex1 + hex2;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Simple 32-bit hash for basic use cases
|
|
165
|
+
*/
|
|
166
|
+
function hash32(input) {
|
|
167
|
+
let hash = 0;
|
|
168
|
+
if (input.length === 0)
|
|
169
|
+
return hash.toString(36);
|
|
170
|
+
for (let i = 0; i < input.length; i++) {
|
|
171
|
+
const char = input.charCodeAt(i);
|
|
172
|
+
hash = ((hash << 5) - hash) + char;
|
|
173
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
174
|
+
}
|
|
175
|
+
return Math.abs(hash).toString(36);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Hash fingerprint components into a stable identifier
|
|
179
|
+
*/
|
|
180
|
+
function hashFingerprint(components) {
|
|
181
|
+
// Convert components to canonical string representation
|
|
182
|
+
const canonical = JSON.stringify(components, Object.keys(components).sort());
|
|
183
|
+
// Use MurmurHash3 x64 for consistent hashing
|
|
184
|
+
return x64hash128(canonical);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Generate a visitor ID from fingerprint hash
|
|
188
|
+
*/
|
|
189
|
+
function generateVisitorId(fingerprintHash) {
|
|
190
|
+
// Use first 16 characters of the hash for visitor ID
|
|
191
|
+
return 'vis_' + fingerprintHash.substring(0, 16);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Canvas Fingerprinting
|
|
196
|
+
* Based on FingerprintJS canvas component with incognito detection
|
|
197
|
+
*/
|
|
198
|
+
/**
|
|
199
|
+
* Text to render for canvas fingerprinting
|
|
200
|
+
*/
|
|
201
|
+
const CANVAS_TEXT = 'RabbitTracker Canvas 🎨 🔒 2024';
|
|
202
|
+
/**
|
|
203
|
+
* Geometric shapes for canvas fingerprinting
|
|
204
|
+
*/
|
|
205
|
+
function drawGeometry(ctx) {
|
|
206
|
+
// Set up styles
|
|
207
|
+
ctx.fillStyle = 'rgb(102, 204, 0)';
|
|
208
|
+
ctx.fillRect(10, 10, 50, 50);
|
|
209
|
+
ctx.fillStyle = '#f60';
|
|
210
|
+
ctx.fillRect(70, 10, 50, 50);
|
|
211
|
+
// Draw circle
|
|
212
|
+
ctx.beginPath();
|
|
213
|
+
ctx.arc(50, 80, 20, 0, Math.PI * 2, true);
|
|
214
|
+
ctx.closePath();
|
|
215
|
+
ctx.fill();
|
|
216
|
+
// Draw triangle
|
|
217
|
+
ctx.beginPath();
|
|
218
|
+
ctx.moveTo(100, 80);
|
|
219
|
+
ctx.lineTo(120, 120);
|
|
220
|
+
ctx.lineTo(80, 120);
|
|
221
|
+
ctx.closePath();
|
|
222
|
+
ctx.stroke();
|
|
223
|
+
// Add gradient
|
|
224
|
+
const gradient = ctx.createLinearGradient(0, 0, 150, 150);
|
|
225
|
+
gradient.addColorStop(0, 'red');
|
|
226
|
+
gradient.addColorStop(1, 'blue');
|
|
227
|
+
ctx.fillStyle = gradient;
|
|
228
|
+
ctx.fillRect(130, 10, 50, 50);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Draw text with various styles
|
|
232
|
+
*/
|
|
233
|
+
function drawText(ctx) {
|
|
234
|
+
// Text with emoji and special characters
|
|
235
|
+
ctx.textBaseline = 'top';
|
|
236
|
+
ctx.font = '14px Arial, sans-serif';
|
|
237
|
+
ctx.fillStyle = '#000';
|
|
238
|
+
ctx.fillText(CANVAS_TEXT, 4, 140);
|
|
239
|
+
// Different font
|
|
240
|
+
ctx.font = '12px Georgia, serif';
|
|
241
|
+
ctx.fillStyle = '#666';
|
|
242
|
+
ctx.fillText('Georgia Font Test', 4, 160);
|
|
243
|
+
// Bold text
|
|
244
|
+
ctx.font = 'bold 16px Helvetica';
|
|
245
|
+
ctx.fillStyle = '#333';
|
|
246
|
+
ctx.fillText('Bold Helvetica', 4, 180);
|
|
247
|
+
// Apply transformations
|
|
248
|
+
ctx.save();
|
|
249
|
+
ctx.scale(1.2, 0.8);
|
|
250
|
+
ctx.font = '10px Courier New';
|
|
251
|
+
ctx.fillStyle = '#999';
|
|
252
|
+
ctx.fillText('Transformed Text', 4, 220);
|
|
253
|
+
ctx.restore();
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Detect potential incognito mode through canvas inconsistencies
|
|
257
|
+
*/
|
|
258
|
+
function detectInconsistencies(textData, geometryData, ctx) {
|
|
259
|
+
try {
|
|
260
|
+
// Test if getImageData works consistently
|
|
261
|
+
const imageData = ctx.getImageData(0, 0, 1, 1);
|
|
262
|
+
// In some browsers in incognito mode, canvas operations might be slightly different
|
|
263
|
+
// This is a heuristic check
|
|
264
|
+
if (textData.length < 100 || geometryData.length < 100) {
|
|
265
|
+
return true; // Suspiciously short data
|
|
266
|
+
}
|
|
267
|
+
// Check for specific patterns that might indicate canvas blocking
|
|
268
|
+
const hasExpectedPatterns = textData.includes('data:image/png') &&
|
|
269
|
+
geometryData.includes('data:image/png');
|
|
270
|
+
return !hasExpectedPatterns;
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
// If getImageData fails, it might indicate blocking
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Generate canvas fingerprint
|
|
279
|
+
*/
|
|
280
|
+
async function getCanvasFingerprint() {
|
|
281
|
+
const startTime = performance.now();
|
|
282
|
+
try {
|
|
283
|
+
// Create canvas element
|
|
284
|
+
const canvas = document.createElement('canvas');
|
|
285
|
+
canvas.width = 200;
|
|
286
|
+
canvas.height = 250;
|
|
287
|
+
const ctx = canvas.getContext('2d');
|
|
288
|
+
if (!ctx) {
|
|
289
|
+
throw new Error('Canvas 2D context not available');
|
|
290
|
+
}
|
|
291
|
+
// Test canvas winding (different behavior across browsers)
|
|
292
|
+
ctx.rect(0, 0, 10, 10);
|
|
293
|
+
ctx.rect(2, 2, 6, 6);
|
|
294
|
+
const isClockwise = ctx.isPointInPath(5, 5, 'evenodd');
|
|
295
|
+
// Draw text
|
|
296
|
+
drawText(ctx);
|
|
297
|
+
const textData = canvas.toDataURL();
|
|
298
|
+
// Clear and draw geometry
|
|
299
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
300
|
+
drawGeometry(ctx);
|
|
301
|
+
const geometryData = canvas.toDataURL();
|
|
302
|
+
// Check for inconsistencies that might indicate incognito mode
|
|
303
|
+
const isInconsistent = detectInconsistencies(textData, geometryData, ctx);
|
|
304
|
+
const endTime = performance.now();
|
|
305
|
+
const result = {
|
|
306
|
+
text: hash32(textData),
|
|
307
|
+
geometry: hash32(geometryData),
|
|
308
|
+
winding: isClockwise,
|
|
309
|
+
isInconsistent
|
|
310
|
+
};
|
|
311
|
+
return {
|
|
312
|
+
value: result,
|
|
313
|
+
duration: endTime - startTime
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
return {
|
|
318
|
+
value: {
|
|
319
|
+
text: 'error',
|
|
320
|
+
geometry: 'error',
|
|
321
|
+
winding: false,
|
|
322
|
+
isInconsistent: true
|
|
323
|
+
},
|
|
324
|
+
duration: performance.now() - startTime,
|
|
325
|
+
error: error instanceof Error ? error.message : 'Canvas fingerprinting failed'
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Check if canvas fingerprinting is available
|
|
331
|
+
*/
|
|
332
|
+
function isCanvasAvailable$1() {
|
|
333
|
+
try {
|
|
334
|
+
const canvas = document.createElement('canvas');
|
|
335
|
+
const ctx = canvas.getContext('2d');
|
|
336
|
+
return ctx !== null && typeof ctx.fillText === 'function';
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* WebGL Fingerprinting
|
|
345
|
+
* Based on FingerprintJS WebGL component for GPU and driver detection
|
|
346
|
+
*/
|
|
347
|
+
/**
|
|
348
|
+
* WebGL parameter constants for fingerprinting
|
|
349
|
+
*/
|
|
350
|
+
const WEBGL_PARAMS = {
|
|
351
|
+
VENDOR: 0x1f00,
|
|
352
|
+
RENDERER: 0x1f01,
|
|
353
|
+
VERSION: 0x1f02,
|
|
354
|
+
SHADING_LANGUAGE_VERSION: 0x8b8c,
|
|
355
|
+
MAX_VERTEX_ATTRIBS: 0x8869,
|
|
356
|
+
MAX_TEXTURE_SIZE: 0x0d33,
|
|
357
|
+
MAX_RENDERBUFFER_SIZE: 0x84e8,
|
|
358
|
+
MAX_VIEWPORT_DIMS: 0x0d3a,
|
|
359
|
+
ALIASED_LINE_WIDTH_RANGE: 0x846e,
|
|
360
|
+
ALIASED_POINT_SIZE_RANGE: 0x846d,
|
|
361
|
+
MAX_FRAGMENT_UNIFORM_VECTORS: 0x8dfd,
|
|
362
|
+
MAX_VERTEX_UNIFORM_VECTORS: 0x8dfb,
|
|
363
|
+
MAX_VARYING_VECTORS: 0x8dfc,
|
|
364
|
+
MAX_VERTEX_TEXTURE_IMAGE_UNITS: 0x8b4c,
|
|
365
|
+
MAX_TEXTURE_IMAGE_UNITS: 0x8872,
|
|
366
|
+
MAX_COMBINED_TEXTURE_IMAGE_UNITS: 0x8b4d,
|
|
367
|
+
};
|
|
368
|
+
/**
|
|
369
|
+
* Get WebGL context with fallback
|
|
370
|
+
*/
|
|
371
|
+
function getWebGLContext() {
|
|
372
|
+
const canvas = document.createElement('canvas');
|
|
373
|
+
// Try different context types
|
|
374
|
+
const contextTypes = ['webgl', 'experimental-webgl', 'webkit-3d', 'moz-webgl'];
|
|
375
|
+
for (const contextType of contextTypes) {
|
|
376
|
+
try {
|
|
377
|
+
const gl = canvas.getContext(contextType);
|
|
378
|
+
if (gl)
|
|
379
|
+
return gl;
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
// Continue trying other context types
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Get WebGL extensions
|
|
389
|
+
*/
|
|
390
|
+
function getWebGLExtensions(gl) {
|
|
391
|
+
const extensions = [];
|
|
392
|
+
try {
|
|
393
|
+
const supportedExtensions = gl.getSupportedExtensions();
|
|
394
|
+
if (supportedExtensions) {
|
|
395
|
+
extensions.push(...supportedExtensions.sort());
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
// Extensions not available
|
|
400
|
+
}
|
|
401
|
+
return extensions;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Get WebGL parameter value safely
|
|
405
|
+
*/
|
|
406
|
+
function getWebGLParameter(gl, param) {
|
|
407
|
+
try {
|
|
408
|
+
return gl.getParameter(param);
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Get shader precision information
|
|
416
|
+
*/
|
|
417
|
+
function getShaderPrecision(gl) {
|
|
418
|
+
const vertex = [];
|
|
419
|
+
const fragment = [];
|
|
420
|
+
try {
|
|
421
|
+
const precisionTypes = [gl.LOW_FLOAT, gl.MEDIUM_FLOAT, gl.HIGH_FLOAT, gl.LOW_INT, gl.MEDIUM_INT, gl.HIGH_INT];
|
|
422
|
+
const precisionNames = ['LOW_FLOAT', 'MEDIUM_FLOAT', 'HIGH_FLOAT', 'LOW_INT', 'MEDIUM_INT', 'HIGH_INT'];
|
|
423
|
+
for (let i = 0; i < precisionTypes.length; i++) {
|
|
424
|
+
const precisionType = precisionTypes[i];
|
|
425
|
+
if (precisionType === undefined)
|
|
426
|
+
continue;
|
|
427
|
+
const vertexPrecision = gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, precisionType);
|
|
428
|
+
const fragmentPrecision = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, precisionType);
|
|
429
|
+
if (vertexPrecision) {
|
|
430
|
+
vertex.push(`${precisionNames[i]}:${vertexPrecision.precision},${vertexPrecision.rangeMin},${vertexPrecision.rangeMax}`);
|
|
431
|
+
}
|
|
432
|
+
if (fragmentPrecision) {
|
|
433
|
+
fragment.push(`${precisionNames[i]}:${fragmentPrecision.precision},${fragmentPrecision.rangeMin},${fragmentPrecision.rangeMax}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
// Precision format not available
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
vertex: vertex.join(';'),
|
|
442
|
+
fragment: fragment.join(';')
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Collect WebGL parameters for fingerprinting
|
|
447
|
+
*/
|
|
448
|
+
function collectWebGLParameters(gl) {
|
|
449
|
+
const parameters = {};
|
|
450
|
+
// Collect standard parameters
|
|
451
|
+
Object.entries(WEBGL_PARAMS).forEach(([name, param]) => {
|
|
452
|
+
const value = getWebGLParameter(gl, param);
|
|
453
|
+
if (value !== null) {
|
|
454
|
+
// Convert arrays to strings for consistent serialization
|
|
455
|
+
parameters[name] = Array.isArray(value) ? value.join(',') : value;
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
// Additional parameters that might be available
|
|
459
|
+
const additionalParams = [
|
|
460
|
+
'UNMASKED_VENDOR_WEBGL',
|
|
461
|
+
'UNMASKED_RENDERER_WEBGL'
|
|
462
|
+
];
|
|
463
|
+
additionalParams.forEach(paramName => {
|
|
464
|
+
try {
|
|
465
|
+
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
|
|
466
|
+
if (debugInfo) {
|
|
467
|
+
const param = debugInfo[paramName];
|
|
468
|
+
if (param !== undefined) {
|
|
469
|
+
const value = gl.getParameter(param);
|
|
470
|
+
if (value) {
|
|
471
|
+
parameters[paramName] = value;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
// Debug renderer info not available
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
return parameters;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Generate WebGL fingerprint
|
|
484
|
+
*/
|
|
485
|
+
async function getWebGLFingerprint() {
|
|
486
|
+
const startTime = performance.now();
|
|
487
|
+
try {
|
|
488
|
+
const gl = getWebGLContext();
|
|
489
|
+
if (!gl) {
|
|
490
|
+
throw new Error('WebGL not available');
|
|
491
|
+
}
|
|
492
|
+
// Get basic WebGL information
|
|
493
|
+
const vendor = getWebGLParameter(gl, gl.VENDOR) || 'unknown';
|
|
494
|
+
const renderer = getWebGLParameter(gl, gl.RENDERER) || 'unknown';
|
|
495
|
+
const version = getWebGLParameter(gl, gl.VERSION) || 'unknown';
|
|
496
|
+
// Get unmasked vendor/renderer if available (more specific GPU info)
|
|
497
|
+
let unmaskedVendor = vendor;
|
|
498
|
+
let unmaskedRenderer = renderer;
|
|
499
|
+
try {
|
|
500
|
+
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
|
|
501
|
+
if (debugInfo) {
|
|
502
|
+
unmaskedVendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) || vendor;
|
|
503
|
+
unmaskedRenderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) || renderer;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
// Debug renderer info blocked or not available
|
|
508
|
+
}
|
|
509
|
+
// Collect extensions
|
|
510
|
+
const extensions = getWebGLExtensions(gl);
|
|
511
|
+
// Collect parameters
|
|
512
|
+
const parameters = collectWebGLParameters(gl);
|
|
513
|
+
// Get shader precision
|
|
514
|
+
const shaderPrecision = getShaderPrecision(gl);
|
|
515
|
+
const endTime = performance.now();
|
|
516
|
+
const result = {
|
|
517
|
+
vendor: unmaskedVendor,
|
|
518
|
+
renderer: unmaskedRenderer,
|
|
519
|
+
version,
|
|
520
|
+
extensions,
|
|
521
|
+
parameters,
|
|
522
|
+
shaderPrecision
|
|
523
|
+
};
|
|
524
|
+
return {
|
|
525
|
+
value: result,
|
|
526
|
+
duration: endTime - startTime
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
return {
|
|
531
|
+
value: {
|
|
532
|
+
vendor: 'unknown',
|
|
533
|
+
renderer: 'unknown',
|
|
534
|
+
version: 'unknown',
|
|
535
|
+
extensions: [],
|
|
536
|
+
parameters: {},
|
|
537
|
+
shaderPrecision: { vertex: '', fragment: '' }
|
|
538
|
+
},
|
|
539
|
+
duration: performance.now() - startTime,
|
|
540
|
+
error: error instanceof Error ? error.message : 'WebGL fingerprinting failed'
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Check if WebGL fingerprinting is available
|
|
546
|
+
*/
|
|
547
|
+
function isWebGLAvailable() {
|
|
548
|
+
try {
|
|
549
|
+
const canvas = document.createElement('canvas');
|
|
550
|
+
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
|
551
|
+
return gl !== null;
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Audio Fingerprinting
|
|
560
|
+
* Based on FingerprintJS audio component using AudioContext
|
|
561
|
+
*/
|
|
562
|
+
/**
|
|
563
|
+
* Audio configuration for fingerprinting
|
|
564
|
+
*/
|
|
565
|
+
const AUDIO_CONFIG = {
|
|
566
|
+
SAMPLE_RATE: 44100,
|
|
567
|
+
DURATION: 0.1, // 100ms
|
|
568
|
+
FREQUENCY: 1000, // 1kHz tone
|
|
569
|
+
COMPRESSOR_THRESHOLD: -50,
|
|
570
|
+
COMPRESSOR_KNEE: 40,
|
|
571
|
+
COMPRESSOR_RATIO: 12,
|
|
572
|
+
COMPRESSOR_ATTACK: 0.003,
|
|
573
|
+
COMPRESSOR_RELEASE: 0.25,
|
|
574
|
+
};
|
|
575
|
+
/**
|
|
576
|
+
* Create audio context with fallbacks
|
|
577
|
+
*/
|
|
578
|
+
function createAudioContext() {
|
|
579
|
+
try {
|
|
580
|
+
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
|
|
581
|
+
if (!AudioContextClass)
|
|
582
|
+
return null;
|
|
583
|
+
return new AudioContextClass();
|
|
584
|
+
}
|
|
585
|
+
catch {
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Generate oscillator fingerprint
|
|
591
|
+
*/
|
|
592
|
+
async function generateOscillatorFingerprint(audioContext) {
|
|
593
|
+
const oscillator = audioContext.createOscillator();
|
|
594
|
+
const compressor = audioContext.createDynamicsCompressor();
|
|
595
|
+
const buffer = audioContext.createBuffer(1, audioContext.sampleRate * AUDIO_CONFIG.DURATION, audioContext.sampleRate);
|
|
596
|
+
// Configure oscillator
|
|
597
|
+
oscillator.type = 'triangle';
|
|
598
|
+
oscillator.frequency.setValueAtTime(AUDIO_CONFIG.FREQUENCY, audioContext.currentTime);
|
|
599
|
+
// Configure compressor
|
|
600
|
+
compressor.threshold.setValueAtTime(AUDIO_CONFIG.COMPRESSOR_THRESHOLD, audioContext.currentTime);
|
|
601
|
+
compressor.knee.setValueAtTime(AUDIO_CONFIG.COMPRESSOR_KNEE, audioContext.currentTime);
|
|
602
|
+
compressor.ratio.setValueAtTime(AUDIO_CONFIG.COMPRESSOR_RATIO, audioContext.currentTime);
|
|
603
|
+
compressor.attack.setValueAtTime(AUDIO_CONFIG.COMPRESSOR_ATTACK, audioContext.currentTime);
|
|
604
|
+
compressor.release.setValueAtTime(AUDIO_CONFIG.COMPRESSOR_RELEASE, audioContext.currentTime);
|
|
605
|
+
// Connect nodes
|
|
606
|
+
oscillator.connect(compressor);
|
|
607
|
+
compressor.connect(audioContext.destination);
|
|
608
|
+
// Start and stop oscillator
|
|
609
|
+
oscillator.start(audioContext.currentTime);
|
|
610
|
+
oscillator.stop(audioContext.currentTime + AUDIO_CONFIG.DURATION);
|
|
611
|
+
// Wait for processing and get buffer data
|
|
612
|
+
await new Promise(resolve => setTimeout(resolve, AUDIO_CONFIG.DURATION * 1000 + 50));
|
|
613
|
+
// Generate fingerprint from audio characteristics
|
|
614
|
+
const channelData = buffer.getChannelData(0);
|
|
615
|
+
const samples = Array.from(channelData.slice(0, 100)); // First 100 samples
|
|
616
|
+
return hash32(samples.join(','));
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Generate compressor fingerprint
|
|
620
|
+
*/
|
|
621
|
+
async function generateCompressorFingerprint(audioContext) {
|
|
622
|
+
try {
|
|
623
|
+
const oscillator = audioContext.createOscillator();
|
|
624
|
+
const compressor = audioContext.createDynamicsCompressor();
|
|
625
|
+
const gain = audioContext.createGain();
|
|
626
|
+
// Create offline context for deterministic results
|
|
627
|
+
const offlineContext = new OfflineAudioContext(1, audioContext.sampleRate * AUDIO_CONFIG.DURATION, audioContext.sampleRate);
|
|
628
|
+
const offlineOscillator = offlineContext.createOscillator();
|
|
629
|
+
const offlineCompressor = offlineContext.createDynamicsCompressor();
|
|
630
|
+
// Configure nodes
|
|
631
|
+
offlineOscillator.type = 'triangle';
|
|
632
|
+
offlineOscillator.frequency.setValueAtTime(AUDIO_CONFIG.FREQUENCY, offlineContext.currentTime);
|
|
633
|
+
offlineCompressor.threshold.setValueAtTime(AUDIO_CONFIG.COMPRESSOR_THRESHOLD, offlineContext.currentTime);
|
|
634
|
+
offlineCompressor.knee.setValueAtTime(AUDIO_CONFIG.COMPRESSOR_KNEE, offlineContext.currentTime);
|
|
635
|
+
offlineCompressor.ratio.setValueAtTime(AUDIO_CONFIG.COMPRESSOR_RATIO, offlineContext.currentTime);
|
|
636
|
+
offlineCompressor.attack.setValueAtTime(AUDIO_CONFIG.COMPRESSOR_ATTACK, offlineContext.currentTime);
|
|
637
|
+
offlineCompressor.release.setValueAtTime(AUDIO_CONFIG.COMPRESSOR_RELEASE, offlineContext.currentTime);
|
|
638
|
+
// Connect and render
|
|
639
|
+
offlineOscillator.connect(offlineCompressor);
|
|
640
|
+
offlineCompressor.connect(offlineContext.destination);
|
|
641
|
+
offlineOscillator.start();
|
|
642
|
+
offlineOscillator.stop(AUDIO_CONFIG.DURATION);
|
|
643
|
+
const renderedBuffer = await offlineContext.startRendering();
|
|
644
|
+
const channelData = renderedBuffer.getChannelData(0);
|
|
645
|
+
// Create fingerprint from specific samples
|
|
646
|
+
const fingerprintSamples = [
|
|
647
|
+
channelData[0] || 0,
|
|
648
|
+
channelData[Math.floor(channelData.length * 0.25)] || 0,
|
|
649
|
+
channelData[Math.floor(channelData.length * 0.5)] || 0,
|
|
650
|
+
channelData[Math.floor(channelData.length * 0.75)] || 0,
|
|
651
|
+
channelData[channelData.length - 1] || 0
|
|
652
|
+
];
|
|
653
|
+
return hash32(fingerprintSamples.join(','));
|
|
654
|
+
}
|
|
655
|
+
catch (error) {
|
|
656
|
+
// Fallback for browsers without OfflineAudioContext
|
|
657
|
+
return hash32('compressor_fallback');
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Generate audio fingerprint
|
|
662
|
+
*/
|
|
663
|
+
async function getAudioFingerprint() {
|
|
664
|
+
const startTime = performance.now();
|
|
665
|
+
try {
|
|
666
|
+
const audioContext = createAudioContext();
|
|
667
|
+
if (!audioContext) {
|
|
668
|
+
throw new Error('AudioContext not available');
|
|
669
|
+
}
|
|
670
|
+
// Resume context if needed (Chrome autoplay policy)
|
|
671
|
+
if (audioContext.state === 'suspended') {
|
|
672
|
+
try {
|
|
673
|
+
await audioContext.resume();
|
|
674
|
+
}
|
|
675
|
+
catch {
|
|
676
|
+
// Cannot resume context
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
// Generate fingerprints
|
|
680
|
+
const [oscillatorHash, compressorHash] = await Promise.all([
|
|
681
|
+
generateOscillatorFingerprint(audioContext).catch(() => 'oscillator_error'),
|
|
682
|
+
generateCompressorFingerprint(audioContext).catch(() => 'compressor_error')
|
|
683
|
+
]);
|
|
684
|
+
// Close audio context to free resources
|
|
685
|
+
try {
|
|
686
|
+
await audioContext.close();
|
|
687
|
+
}
|
|
688
|
+
catch {
|
|
689
|
+
// Context close failed
|
|
690
|
+
}
|
|
691
|
+
const endTime = performance.now();
|
|
692
|
+
const result = {
|
|
693
|
+
oscillator: oscillatorHash,
|
|
694
|
+
compressor: compressorHash,
|
|
695
|
+
sampleRate: audioContext.sampleRate,
|
|
696
|
+
maxChannelCount: audioContext.destination.maxChannelCount
|
|
697
|
+
};
|
|
698
|
+
return {
|
|
699
|
+
value: result,
|
|
700
|
+
duration: endTime - startTime
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
catch (error) {
|
|
704
|
+
return {
|
|
705
|
+
value: {
|
|
706
|
+
oscillator: 'error',
|
|
707
|
+
compressor: 'error',
|
|
708
|
+
sampleRate: 0,
|
|
709
|
+
maxChannelCount: 0
|
|
710
|
+
},
|
|
711
|
+
duration: performance.now() - startTime,
|
|
712
|
+
error: error instanceof Error ? error.message : 'Audio fingerprinting failed'
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Check if audio fingerprinting is available
|
|
718
|
+
*/
|
|
719
|
+
function isAudioAvailable() {
|
|
720
|
+
try {
|
|
721
|
+
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
|
|
722
|
+
return typeof AudioContextClass === 'function';
|
|
723
|
+
}
|
|
724
|
+
catch {
|
|
725
|
+
return false;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Font Detection Fingerprinting
|
|
731
|
+
* Based on FingerprintJS font detection techniques
|
|
732
|
+
*/
|
|
733
|
+
/**
|
|
734
|
+
* Font list for detection (commonly available system fonts)
|
|
735
|
+
*/
|
|
736
|
+
const FONT_LIST = [
|
|
737
|
+
// Windows fonts
|
|
738
|
+
'Arial', 'Arial Black', 'Calibri', 'Cambria', 'Comic Sans MS', 'Consolas',
|
|
739
|
+
'Courier New', 'Georgia', 'Impact', 'Lucida Console', 'Lucida Sans Unicode',
|
|
740
|
+
'Microsoft Sans Serif', 'Palatino Linotype', 'Segoe UI', 'Tahoma', 'Times New Roman',
|
|
741
|
+
'Trebuchet MS', 'Verdana',
|
|
742
|
+
// macOS fonts
|
|
743
|
+
'American Typewriter', 'Avenir', 'Baskerville', 'Big Caslon', 'Brush Script MT',
|
|
744
|
+
'Copperplate', 'Didot', 'Futura', 'Gill Sans', 'Helvetica', 'Helvetica Neue',
|
|
745
|
+
'Hoefler Text', 'Lucida Grande', 'Marker Felt', 'Optima', 'Papyrus',
|
|
746
|
+
'Phosphate', 'Rockwell', 'Savoye LET', 'SignPainter', 'Skia', 'Snell Roundhand',
|
|
747
|
+
'System Font', 'Zapfino',
|
|
748
|
+
// Linux fonts
|
|
749
|
+
'DejaVu Sans', 'DejaVu Sans Mono', 'DejaVu Serif', 'Droid Sans', 'Droid Sans Mono',
|
|
750
|
+
'Droid Serif', 'Liberation Sans', 'Liberation Sans Narrow', 'Liberation Serif',
|
|
751
|
+
'Ubuntu', 'Ubuntu Mono',
|
|
752
|
+
// Android fonts
|
|
753
|
+
'Roboto', 'Roboto Condensed', 'Roboto Mono', 'Roboto Slab', 'Droid Sans',
|
|
754
|
+
'Droid Sans Mono', 'Droid Serif',
|
|
755
|
+
// iOS fonts
|
|
756
|
+
'Helvetica Neue', 'Arial', 'Helvetica', 'Courier New', 'Times New Roman',
|
|
757
|
+
'San Francisco', 'Avenir Next',
|
|
758
|
+
// Common web fonts
|
|
759
|
+
'Open Sans', 'Lato', 'Montserrat', 'Source Sans Pro', 'Raleway', 'PT Sans',
|
|
760
|
+
'Lora', 'Playfair Display', 'Oswald', 'Slabo 27px', 'Fira Sans',
|
|
761
|
+
// East Asian fonts
|
|
762
|
+
'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Meiryo', 'MS Gothic', 'MS Mincho',
|
|
763
|
+
'SimSun', 'SimHei', 'Microsoft YaHei', 'Malgun Gothic', 'Apple SD Gothic Neo'
|
|
764
|
+
];
|
|
765
|
+
/**
|
|
766
|
+
* Fallback fonts for measurement
|
|
767
|
+
*/
|
|
768
|
+
const FALLBACK_FONTS = ['monospace', 'sans-serif', 'serif'];
|
|
769
|
+
/**
|
|
770
|
+
* Test text for font measurement
|
|
771
|
+
*/
|
|
772
|
+
const TEST_TEXT = 'mmMwWLliI0O&1 ※';
|
|
773
|
+
/**
|
|
774
|
+
* Font measurement using canvas
|
|
775
|
+
*/
|
|
776
|
+
function measureText(text, fontFamily) {
|
|
777
|
+
const canvas = document.createElement('canvas');
|
|
778
|
+
const ctx = canvas.getContext('2d');
|
|
779
|
+
if (!ctx) {
|
|
780
|
+
return { width: 0, height: 0 };
|
|
781
|
+
}
|
|
782
|
+
ctx.font = `12px ${fontFamily}`;
|
|
783
|
+
const metrics = ctx.measureText(text);
|
|
784
|
+
return {
|
|
785
|
+
width: metrics.width,
|
|
786
|
+
height: metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Font measurement using DOM (fallback method)
|
|
791
|
+
*/
|
|
792
|
+
function measureTextDOM(text, fontFamily) {
|
|
793
|
+
const element = document.createElement('span');
|
|
794
|
+
element.style.position = 'absolute';
|
|
795
|
+
element.style.left = '-9999px';
|
|
796
|
+
element.style.top = '-9999px';
|
|
797
|
+
element.style.fontSize = '12px';
|
|
798
|
+
element.style.fontFamily = fontFamily;
|
|
799
|
+
element.style.whiteSpace = 'nowrap';
|
|
800
|
+
element.textContent = text;
|
|
801
|
+
document.body.appendChild(element);
|
|
802
|
+
const rect = element.getBoundingClientRect();
|
|
803
|
+
const result = {
|
|
804
|
+
width: rect.width,
|
|
805
|
+
height: rect.height
|
|
806
|
+
};
|
|
807
|
+
document.body.removeChild(element);
|
|
808
|
+
return result;
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Detect if a font is available by comparing measurements
|
|
812
|
+
*/
|
|
813
|
+
function isFontAvailable(fontName, method = 'canvas') {
|
|
814
|
+
const measureFunc = method === 'canvas' ? measureText : measureTextDOM;
|
|
815
|
+
// Measure with fallback fonts first
|
|
816
|
+
const fallbackMeasurements = FALLBACK_FONTS.map(fallback => measureFunc(TEST_TEXT, fallback));
|
|
817
|
+
// Measure with target font + fallback
|
|
818
|
+
const targetMeasurements = FALLBACK_FONTS.map(fallback => measureFunc(TEST_TEXT, `${fontName}, ${fallback}`));
|
|
819
|
+
// Compare measurements - if font is available, measurements should differ
|
|
820
|
+
for (let i = 0; i < fallbackMeasurements.length; i++) {
|
|
821
|
+
const fallback = fallbackMeasurements[i];
|
|
822
|
+
const target = targetMeasurements[i];
|
|
823
|
+
if (fallback && target) {
|
|
824
|
+
// Allow small tolerance for measurement differences
|
|
825
|
+
const widthDiff = Math.abs(fallback.width - target.width);
|
|
826
|
+
const heightDiff = Math.abs(fallback.height - target.height);
|
|
827
|
+
if (widthDiff > 0.1 || heightDiff > 0.1) {
|
|
828
|
+
return true; // Font likely available
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
return false;
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Advanced font detection with measurements
|
|
836
|
+
*/
|
|
837
|
+
function detectFontsWithMeasurements() {
|
|
838
|
+
const available = [];
|
|
839
|
+
const measurements = {};
|
|
840
|
+
for (const font of FONT_LIST) {
|
|
841
|
+
try {
|
|
842
|
+
const measurement = measureText(TEST_TEXT, font);
|
|
843
|
+
measurements[font] = measurement;
|
|
844
|
+
if (isFontAvailable(font, 'canvas')) {
|
|
845
|
+
available.push(font);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
catch (error) {
|
|
849
|
+
// Skip fonts that cause errors
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
return { available, measurements };
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Basic font detection (faster but less detailed)
|
|
857
|
+
*/
|
|
858
|
+
function detectFontsBasic() {
|
|
859
|
+
const available = [];
|
|
860
|
+
for (const font of FONT_LIST) {
|
|
861
|
+
try {
|
|
862
|
+
if (isFontAvailable(font, 'dom')) {
|
|
863
|
+
available.push(font);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
catch (error) {
|
|
867
|
+
// Skip fonts that cause errors
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
return available;
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Generate font fingerprint
|
|
875
|
+
*/
|
|
876
|
+
async function getFontFingerprint(useAdvanced = true) {
|
|
877
|
+
const startTime = performance.now();
|
|
878
|
+
try {
|
|
879
|
+
let result;
|
|
880
|
+
if (useAdvanced && isCanvasAvailable()) {
|
|
881
|
+
// Advanced detection with measurements
|
|
882
|
+
const { available, measurements } = detectFontsWithMeasurements();
|
|
883
|
+
result = {
|
|
884
|
+
available: available.sort(),
|
|
885
|
+
method: 'advanced',
|
|
886
|
+
measurements
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
else {
|
|
890
|
+
// Basic detection
|
|
891
|
+
const available = detectFontsBasic();
|
|
892
|
+
result = {
|
|
893
|
+
available: available.sort(),
|
|
894
|
+
method: 'basic'
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
const endTime = performance.now();
|
|
898
|
+
return {
|
|
899
|
+
value: result,
|
|
900
|
+
duration: endTime - startTime
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
catch (error) {
|
|
904
|
+
return {
|
|
905
|
+
value: {
|
|
906
|
+
available: [],
|
|
907
|
+
method: 'basic'
|
|
908
|
+
},
|
|
909
|
+
duration: performance.now() - startTime,
|
|
910
|
+
error: error instanceof Error ? error.message : 'Font detection failed'
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Check if canvas is available for font measurement
|
|
916
|
+
*/
|
|
917
|
+
function isCanvasAvailable() {
|
|
918
|
+
try {
|
|
919
|
+
const canvas = document.createElement('canvas');
|
|
920
|
+
const ctx = canvas.getContext('2d');
|
|
921
|
+
return ctx !== null && typeof ctx.measureText === 'function';
|
|
922
|
+
}
|
|
923
|
+
catch {
|
|
924
|
+
return false;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Check if font detection is available
|
|
929
|
+
*/
|
|
930
|
+
function isFontDetectionAvailable() {
|
|
931
|
+
try {
|
|
932
|
+
// Check if we can create DOM elements for measurement
|
|
933
|
+
const element = document.createElement('span');
|
|
934
|
+
return element && typeof element.style === 'object';
|
|
935
|
+
}
|
|
936
|
+
catch {
|
|
937
|
+
return false;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* Screen and Viewport Fingerprinting
|
|
943
|
+
* Based on FingerprintJS screen components
|
|
944
|
+
*/
|
|
945
|
+
/**
|
|
946
|
+
* Get screen orientation information
|
|
947
|
+
*/
|
|
948
|
+
function getScreenOrientation() {
|
|
949
|
+
try {
|
|
950
|
+
// Modern API
|
|
951
|
+
if (screen.orientation) {
|
|
952
|
+
return {
|
|
953
|
+
angle: screen.orientation.angle,
|
|
954
|
+
type: screen.orientation.type
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
// Legacy API
|
|
958
|
+
const orientation = screen.mozOrientation ||
|
|
959
|
+
screen.msOrientation ||
|
|
960
|
+
screen.webkitOrientation;
|
|
961
|
+
if (orientation !== undefined) {
|
|
962
|
+
return {
|
|
963
|
+
angle: orientation,
|
|
964
|
+
type: 'unknown'
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
// Fallback: estimate from dimensions
|
|
968
|
+
const isLandscape = screen.width > screen.height;
|
|
969
|
+
return {
|
|
970
|
+
angle: isLandscape ? 90 : 0,
|
|
971
|
+
type: isLandscape ? 'landscape-primary' : 'portrait-primary'
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
catch {
|
|
975
|
+
return undefined;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Get device pixel ratio with fallbacks
|
|
980
|
+
*/
|
|
981
|
+
function getPixelRatio() {
|
|
982
|
+
try {
|
|
983
|
+
return window.devicePixelRatio || 1;
|
|
984
|
+
}
|
|
985
|
+
catch {
|
|
986
|
+
return 1;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Get available screen dimensions (excluding taskbars, docks, etc.)
|
|
991
|
+
*/
|
|
992
|
+
function getAvailableScreenDimensions() {
|
|
993
|
+
try {
|
|
994
|
+
return {
|
|
995
|
+
width: screen.availWidth || screen.width,
|
|
996
|
+
height: screen.availHeight || screen.height
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
catch {
|
|
1000
|
+
return {
|
|
1001
|
+
width: screen.width || 0,
|
|
1002
|
+
height: screen.height || 0
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Get viewport dimensions
|
|
1008
|
+
*/
|
|
1009
|
+
function getViewportDimensions() {
|
|
1010
|
+
try {
|
|
1011
|
+
// Use document.documentElement for more accurate measurements
|
|
1012
|
+
const element = document.documentElement;
|
|
1013
|
+
return {
|
|
1014
|
+
width: Math.max(element.clientWidth || 0, window.innerWidth || 0),
|
|
1015
|
+
height: Math.max(element.clientHeight || 0, window.innerHeight || 0)
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
catch {
|
|
1019
|
+
return {
|
|
1020
|
+
width: window.innerWidth || 0,
|
|
1021
|
+
height: window.innerHeight || 0
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Generate screen fingerprint
|
|
1027
|
+
*/
|
|
1028
|
+
async function getScreenFingerprint() {
|
|
1029
|
+
const startTime = performance.now();
|
|
1030
|
+
try {
|
|
1031
|
+
// Get screen dimensions
|
|
1032
|
+
const screenWidth = screen.width || 0;
|
|
1033
|
+
const screenHeight = screen.height || 0;
|
|
1034
|
+
const colorDepth = screen.colorDepth || screen.pixelDepth || 0;
|
|
1035
|
+
// Get available screen space
|
|
1036
|
+
const available = getAvailableScreenDimensions();
|
|
1037
|
+
// Get viewport dimensions
|
|
1038
|
+
const viewport = getViewportDimensions();
|
|
1039
|
+
// Get pixel ratio
|
|
1040
|
+
const pixelRatio = getPixelRatio();
|
|
1041
|
+
// Get orientation
|
|
1042
|
+
const orientation = getScreenOrientation();
|
|
1043
|
+
const endTime = performance.now();
|
|
1044
|
+
const result = {
|
|
1045
|
+
// Screen properties
|
|
1046
|
+
width: screenWidth,
|
|
1047
|
+
height: screenHeight,
|
|
1048
|
+
colorDepth,
|
|
1049
|
+
pixelRatio,
|
|
1050
|
+
// Viewport properties
|
|
1051
|
+
viewportWidth: viewport.width,
|
|
1052
|
+
viewportHeight: viewport.height,
|
|
1053
|
+
// Available screen space
|
|
1054
|
+
availableWidth: available.width,
|
|
1055
|
+
availableHeight: available.height
|
|
1056
|
+
};
|
|
1057
|
+
// Add optional properties conditionally
|
|
1058
|
+
if (orientation) {
|
|
1059
|
+
result.orientation = orientation;
|
|
1060
|
+
}
|
|
1061
|
+
return {
|
|
1062
|
+
value: result,
|
|
1063
|
+
duration: endTime - startTime
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
catch (error) {
|
|
1067
|
+
return {
|
|
1068
|
+
value: {
|
|
1069
|
+
width: 0,
|
|
1070
|
+
height: 0,
|
|
1071
|
+
colorDepth: 0,
|
|
1072
|
+
pixelRatio: 1,
|
|
1073
|
+
viewportWidth: 0,
|
|
1074
|
+
viewportHeight: 0,
|
|
1075
|
+
availableWidth: 0,
|
|
1076
|
+
availableHeight: 0
|
|
1077
|
+
},
|
|
1078
|
+
duration: performance.now() - startTime,
|
|
1079
|
+
error: error instanceof Error ? error.message : 'Screen fingerprinting failed'
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Check if screen fingerprinting is available
|
|
1085
|
+
*/
|
|
1086
|
+
function isScreenAvailable() {
|
|
1087
|
+
try {
|
|
1088
|
+
return typeof screen === 'object' &&
|
|
1089
|
+
typeof screen.width === 'number' &&
|
|
1090
|
+
typeof screen.height === 'number';
|
|
1091
|
+
}
|
|
1092
|
+
catch {
|
|
1093
|
+
return false;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* Browser and System Information Fingerprinting
|
|
1099
|
+
* Based on FingerprintJS browser component with enhanced detection
|
|
1100
|
+
*/
|
|
1101
|
+
/**
|
|
1102
|
+
* Get touch support information
|
|
1103
|
+
*/
|
|
1104
|
+
function getTouchSupport() {
|
|
1105
|
+
try {
|
|
1106
|
+
let maxTouchPoints = 0;
|
|
1107
|
+
let touchEvent = false;
|
|
1108
|
+
let touchStart = false;
|
|
1109
|
+
// Check maxTouchPoints
|
|
1110
|
+
if ('maxTouchPoints' in navigator) {
|
|
1111
|
+
maxTouchPoints = navigator.maxTouchPoints;
|
|
1112
|
+
}
|
|
1113
|
+
else if ('msMaxTouchPoints' in navigator) {
|
|
1114
|
+
maxTouchPoints = navigator.msMaxTouchPoints;
|
|
1115
|
+
}
|
|
1116
|
+
// Check touch events
|
|
1117
|
+
try {
|
|
1118
|
+
touchEvent = 'TouchEvent' in window;
|
|
1119
|
+
}
|
|
1120
|
+
catch {
|
|
1121
|
+
touchEvent = 'ontouchstart' in window;
|
|
1122
|
+
}
|
|
1123
|
+
// Check touch start
|
|
1124
|
+
touchStart = 'ontouchstart' in window;
|
|
1125
|
+
return { maxTouchPoints, touchEvent, touchStart };
|
|
1126
|
+
}
|
|
1127
|
+
catch {
|
|
1128
|
+
return { maxTouchPoints: 0, touchEvent: false, touchStart: false };
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Get plugin information (mostly for legacy browser detection)
|
|
1133
|
+
*/
|
|
1134
|
+
function getPluginInfo() {
|
|
1135
|
+
try {
|
|
1136
|
+
if (!navigator.plugins) {
|
|
1137
|
+
return { length: 0, names: [] };
|
|
1138
|
+
}
|
|
1139
|
+
const plugins = Array.from(navigator.plugins);
|
|
1140
|
+
const names = plugins
|
|
1141
|
+
.map(plugin => plugin.name)
|
|
1142
|
+
.filter(name => name)
|
|
1143
|
+
.sort();
|
|
1144
|
+
return {
|
|
1145
|
+
length: navigator.plugins.length,
|
|
1146
|
+
names: names.slice(0, 10) // Limit to first 10 to avoid huge fingerprints
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
catch {
|
|
1150
|
+
return { length: 0, names: [] };
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Get language and locale information
|
|
1155
|
+
*/
|
|
1156
|
+
function getLanguageInfo() {
|
|
1157
|
+
try {
|
|
1158
|
+
const language = navigator.language || '';
|
|
1159
|
+
let languages = [];
|
|
1160
|
+
if (navigator.languages) {
|
|
1161
|
+
languages = Array.from(navigator.languages);
|
|
1162
|
+
}
|
|
1163
|
+
else if (navigator.language) {
|
|
1164
|
+
languages = [navigator.language];
|
|
1165
|
+
}
|
|
1166
|
+
// Add fallback language properties
|
|
1167
|
+
const additionalLanguages = [
|
|
1168
|
+
navigator.userLanguage,
|
|
1169
|
+
navigator.browserLanguage,
|
|
1170
|
+
navigator.systemLanguage
|
|
1171
|
+
].filter(Boolean);
|
|
1172
|
+
languages = [...new Set([...languages, ...additionalLanguages])];
|
|
1173
|
+
return { language, languages };
|
|
1174
|
+
}
|
|
1175
|
+
catch {
|
|
1176
|
+
return { language: '', languages: [] };
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Get timezone information
|
|
1181
|
+
*/
|
|
1182
|
+
function getTimezoneInfo() {
|
|
1183
|
+
try {
|
|
1184
|
+
let timezone = '';
|
|
1185
|
+
// Modern API
|
|
1186
|
+
if (Intl && Intl.DateTimeFormat) {
|
|
1187
|
+
try {
|
|
1188
|
+
timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
1189
|
+
}
|
|
1190
|
+
catch {
|
|
1191
|
+
// Fallback
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
// Fallback: use timezone offset
|
|
1195
|
+
if (!timezone) {
|
|
1196
|
+
const offset = new Date().getTimezoneOffset();
|
|
1197
|
+
const sign = offset > 0 ? '-' : '+';
|
|
1198
|
+
const hours = Math.floor(Math.abs(offset) / 60);
|
|
1199
|
+
const minutes = Math.abs(offset) % 60;
|
|
1200
|
+
timezone = `UTC${sign}${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
|
|
1201
|
+
}
|
|
1202
|
+
const timezoneOffset = new Date().getTimezoneOffset();
|
|
1203
|
+
return { timezone, timezoneOffset };
|
|
1204
|
+
}
|
|
1205
|
+
catch {
|
|
1206
|
+
return { timezone: '', timezoneOffset: 0 };
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Get hardware information
|
|
1211
|
+
*/
|
|
1212
|
+
function getHardwareInfo() {
|
|
1213
|
+
try {
|
|
1214
|
+
const hardwareConcurrency = navigator.hardwareConcurrency || 0;
|
|
1215
|
+
// Device memory (Chrome-specific)
|
|
1216
|
+
let deviceMemory;
|
|
1217
|
+
if ('deviceMemory' in navigator) {
|
|
1218
|
+
deviceMemory = navigator.deviceMemory;
|
|
1219
|
+
}
|
|
1220
|
+
const result = { hardwareConcurrency };
|
|
1221
|
+
if (deviceMemory !== undefined) {
|
|
1222
|
+
result.deviceMemory = deviceMemory;
|
|
1223
|
+
}
|
|
1224
|
+
return result;
|
|
1225
|
+
}
|
|
1226
|
+
catch {
|
|
1227
|
+
return { hardwareConcurrency: 0 };
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Get platform information with enhanced detection
|
|
1232
|
+
*/
|
|
1233
|
+
function getPlatformInfo() {
|
|
1234
|
+
try {
|
|
1235
|
+
const platform = navigator.platform || '';
|
|
1236
|
+
const userAgent = navigator.userAgent || '';
|
|
1237
|
+
return { platform, userAgent };
|
|
1238
|
+
}
|
|
1239
|
+
catch {
|
|
1240
|
+
return { platform: '', userAgent: '' };
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Get browser capabilities
|
|
1245
|
+
*/
|
|
1246
|
+
function getBrowserCapabilities() {
|
|
1247
|
+
try {
|
|
1248
|
+
const cookieEnabled = navigator.cookieEnabled !== false;
|
|
1249
|
+
// Do Not Track
|
|
1250
|
+
let doNotTrack = null;
|
|
1251
|
+
if ('doNotTrack' in navigator) {
|
|
1252
|
+
doNotTrack = navigator.doNotTrack;
|
|
1253
|
+
}
|
|
1254
|
+
else if ('msDoNotTrack' in navigator) {
|
|
1255
|
+
doNotTrack = navigator.msDoNotTrack;
|
|
1256
|
+
}
|
|
1257
|
+
else if ('mozDoNotTrack' in window) {
|
|
1258
|
+
doNotTrack = window.mozDoNotTrack;
|
|
1259
|
+
}
|
|
1260
|
+
return { cookieEnabled, doNotTrack };
|
|
1261
|
+
}
|
|
1262
|
+
catch {
|
|
1263
|
+
return { cookieEnabled: true, doNotTrack: null };
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Generate browser fingerprint
|
|
1268
|
+
*/
|
|
1269
|
+
async function getBrowserFingerprint() {
|
|
1270
|
+
const startTime = performance.now();
|
|
1271
|
+
try {
|
|
1272
|
+
// Collect all browser information
|
|
1273
|
+
const languageInfo = getLanguageInfo();
|
|
1274
|
+
const timezoneInfo = getTimezoneInfo();
|
|
1275
|
+
const platformInfo = getPlatformInfo();
|
|
1276
|
+
const hardwareInfo = getHardwareInfo();
|
|
1277
|
+
const capabilities = getBrowserCapabilities();
|
|
1278
|
+
const pluginInfo = getPluginInfo();
|
|
1279
|
+
const touchSupport = getTouchSupport();
|
|
1280
|
+
const endTime = performance.now();
|
|
1281
|
+
const result = {
|
|
1282
|
+
// Language and locale
|
|
1283
|
+
language: languageInfo.language,
|
|
1284
|
+
languages: languageInfo.languages,
|
|
1285
|
+
// Timezone
|
|
1286
|
+
timezone: timezoneInfo.timezone,
|
|
1287
|
+
timezoneOffset: timezoneInfo.timezoneOffset,
|
|
1288
|
+
// Platform information
|
|
1289
|
+
platform: platformInfo.platform,
|
|
1290
|
+
userAgent: platformInfo.userAgent,
|
|
1291
|
+
// Hardware information
|
|
1292
|
+
hardwareConcurrency: hardwareInfo.hardwareConcurrency,
|
|
1293
|
+
// Browser capabilities
|
|
1294
|
+
cookieEnabled: capabilities.cookieEnabled,
|
|
1295
|
+
doNotTrack: capabilities.doNotTrack,
|
|
1296
|
+
// Plugin information
|
|
1297
|
+
plugins: pluginInfo,
|
|
1298
|
+
// Touch support
|
|
1299
|
+
touchSupport
|
|
1300
|
+
};
|
|
1301
|
+
// Add optional properties conditionally
|
|
1302
|
+
if (hardwareInfo.deviceMemory !== undefined) {
|
|
1303
|
+
result.deviceMemory = hardwareInfo.deviceMemory;
|
|
1304
|
+
}
|
|
1305
|
+
return {
|
|
1306
|
+
value: result,
|
|
1307
|
+
duration: endTime - startTime
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
catch (error) {
|
|
1311
|
+
return {
|
|
1312
|
+
value: {
|
|
1313
|
+
language: '',
|
|
1314
|
+
languages: [],
|
|
1315
|
+
timezone: '',
|
|
1316
|
+
timezoneOffset: 0,
|
|
1317
|
+
platform: '',
|
|
1318
|
+
userAgent: '',
|
|
1319
|
+
hardwareConcurrency: 0,
|
|
1320
|
+
cookieEnabled: true,
|
|
1321
|
+
doNotTrack: null,
|
|
1322
|
+
plugins: { length: 0, names: [] },
|
|
1323
|
+
touchSupport: { maxTouchPoints: 0, touchEvent: false, touchStart: false }
|
|
1324
|
+
},
|
|
1325
|
+
duration: performance.now() - startTime,
|
|
1326
|
+
error: error instanceof Error ? error.message : 'Browser fingerprinting failed'
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Check if browser fingerprinting is available
|
|
1332
|
+
*/
|
|
1333
|
+
function isBrowserFingerprintingAvailable() {
|
|
1334
|
+
try {
|
|
1335
|
+
return typeof navigator === 'object' && navigator !== null;
|
|
1336
|
+
}
|
|
1337
|
+
catch {
|
|
1338
|
+
return false;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/**
|
|
1343
|
+
* Fingerprint Orchestrator
|
|
1344
|
+
* Main fingerprinting engine that coordinates all components
|
|
1345
|
+
* Based on FingerprintJS architecture with privacy-first approach
|
|
1346
|
+
*/
|
|
1347
|
+
// Import individual fingerprinting components
|
|
1348
|
+
/**
|
|
1349
|
+
* Default fingerprint options (cookieless - memory cache only)
|
|
1350
|
+
*/
|
|
1351
|
+
const DEFAULT_OPTIONS = {
|
|
1352
|
+
gdprMode: false,
|
|
1353
|
+
includeInvasive: true,
|
|
1354
|
+
timeout: 5000,
|
|
1355
|
+
excludeComponents: []
|
|
1356
|
+
};
|
|
1357
|
+
/**
|
|
1358
|
+
* Session-only cache for fingerprint components (memory only, no persistence)
|
|
1359
|
+
*/
|
|
1360
|
+
const sessionCache = new Map();
|
|
1361
|
+
/**
|
|
1362
|
+
* Check if component should be included based on options
|
|
1363
|
+
*/
|
|
1364
|
+
function shouldIncludeComponent(component, options) {
|
|
1365
|
+
// Check if component is excluded
|
|
1366
|
+
if (options.excludeComponents.includes(component)) {
|
|
1367
|
+
return false;
|
|
1368
|
+
}
|
|
1369
|
+
// In GDPR mode, exclude invasive components unless explicitly allowed
|
|
1370
|
+
if (options.gdprMode && !options.includeInvasive) {
|
|
1371
|
+
const invasiveComponents = ['canvas', 'webgl', 'audio'];
|
|
1372
|
+
if (invasiveComponents.includes(component)) {
|
|
1373
|
+
return false;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
return true;
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* Get cached component data from session cache (memory only)
|
|
1380
|
+
*/
|
|
1381
|
+
function getSessionCachedComponent(key) {
|
|
1382
|
+
return sessionCache.get(key) || null;
|
|
1383
|
+
}
|
|
1384
|
+
/**
|
|
1385
|
+
* Cache component data in session memory only
|
|
1386
|
+
*/
|
|
1387
|
+
function setSessionCachedComponent(key, data) {
|
|
1388
|
+
sessionCache.set(key, data);
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* Collect fingerprint component with timeout and session caching
|
|
1392
|
+
*/
|
|
1393
|
+
async function collectComponent(componentType, collector, options) {
|
|
1394
|
+
const cacheKey = `${componentType}_${options.gdprMode}`;
|
|
1395
|
+
// Check session cache first (memory only)
|
|
1396
|
+
const cached = getSessionCachedComponent(cacheKey);
|
|
1397
|
+
if (cached) {
|
|
1398
|
+
return cached;
|
|
1399
|
+
}
|
|
1400
|
+
try {
|
|
1401
|
+
// Create timeout promise
|
|
1402
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1403
|
+
setTimeout(() => reject(new Error(`${componentType} timeout`)), options.timeout);
|
|
1404
|
+
});
|
|
1405
|
+
// Race collection vs timeout
|
|
1406
|
+
const result = await Promise.race([collector(), timeoutPromise]);
|
|
1407
|
+
// Cache successful result in session memory only
|
|
1408
|
+
setSessionCachedComponent(cacheKey, result);
|
|
1409
|
+
return result;
|
|
1410
|
+
}
|
|
1411
|
+
catch (error) {
|
|
1412
|
+
// Return null on error (component will be marked as failed)
|
|
1413
|
+
return null;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Main fingerprint collection function
|
|
1418
|
+
*/
|
|
1419
|
+
async function collectFingerprint(options = {}) {
|
|
1420
|
+
const startTime = performance.now();
|
|
1421
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
1422
|
+
const collectedComponents = [];
|
|
1423
|
+
const failedComponents = [];
|
|
1424
|
+
const fingerprintData = {
|
|
1425
|
+
collectionTime: startTime,
|
|
1426
|
+
gdprMode: opts.gdprMode,
|
|
1427
|
+
components: []
|
|
1428
|
+
};
|
|
1429
|
+
// Collect Canvas fingerprint
|
|
1430
|
+
if (shouldIncludeComponent('canvas', opts) && isCanvasAvailable$1()) {
|
|
1431
|
+
const result = await collectComponent('canvas', getCanvasFingerprint, opts);
|
|
1432
|
+
if (result) {
|
|
1433
|
+
fingerprintData.canvas = result;
|
|
1434
|
+
collectedComponents.push('canvas');
|
|
1435
|
+
}
|
|
1436
|
+
else {
|
|
1437
|
+
failedComponents.push({ component: 'canvas', error: 'Collection failed or timeout' });
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
// Collect WebGL fingerprint
|
|
1441
|
+
if (shouldIncludeComponent('webgl', opts) && isWebGLAvailable()) {
|
|
1442
|
+
const result = await collectComponent('webgl', getWebGLFingerprint, opts);
|
|
1443
|
+
if (result) {
|
|
1444
|
+
fingerprintData.webgl = result;
|
|
1445
|
+
collectedComponents.push('webgl');
|
|
1446
|
+
}
|
|
1447
|
+
else {
|
|
1448
|
+
failedComponents.push({ component: 'webgl', error: 'Collection failed or timeout' });
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
// Collect Audio fingerprint
|
|
1452
|
+
if (shouldIncludeComponent('audio', opts) && isAudioAvailable()) {
|
|
1453
|
+
const result = await collectComponent('audio', getAudioFingerprint, opts);
|
|
1454
|
+
if (result) {
|
|
1455
|
+
fingerprintData.audio = result;
|
|
1456
|
+
collectedComponents.push('audio');
|
|
1457
|
+
}
|
|
1458
|
+
else {
|
|
1459
|
+
failedComponents.push({ component: 'audio', error: 'Collection failed or timeout' });
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
// Collect Font fingerprint
|
|
1463
|
+
if (shouldIncludeComponent('fonts', opts) && isFontDetectionAvailable()) {
|
|
1464
|
+
const result = await collectComponent('fonts', () => getFontFingerprint(!opts.gdprMode), opts);
|
|
1465
|
+
if (result) {
|
|
1466
|
+
fingerprintData.fonts = result;
|
|
1467
|
+
collectedComponents.push('fonts');
|
|
1468
|
+
}
|
|
1469
|
+
else {
|
|
1470
|
+
failedComponents.push({ component: 'fonts', error: 'Collection failed or timeout' });
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
// Collect Screen fingerprint (always available)
|
|
1474
|
+
if (shouldIncludeComponent('screen', opts) && isScreenAvailable()) {
|
|
1475
|
+
const result = await collectComponent('screen', getScreenFingerprint, opts);
|
|
1476
|
+
if (result) {
|
|
1477
|
+
fingerprintData.screen = result;
|
|
1478
|
+
collectedComponents.push('screen');
|
|
1479
|
+
}
|
|
1480
|
+
else {
|
|
1481
|
+
failedComponents.push({ component: 'screen', error: 'Collection failed or timeout' });
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
// Collect Browser fingerprint (always available)
|
|
1485
|
+
if (shouldIncludeComponent('browser', opts) && isBrowserFingerprintingAvailable()) {
|
|
1486
|
+
const result = await collectComponent('browser', getBrowserFingerprint, opts);
|
|
1487
|
+
if (result) {
|
|
1488
|
+
fingerprintData.browser = result;
|
|
1489
|
+
collectedComponents.push('browser');
|
|
1490
|
+
}
|
|
1491
|
+
else {
|
|
1492
|
+
failedComponents.push({ component: 'browser', error: 'Collection failed or timeout' });
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
// Generate hash from collected components
|
|
1496
|
+
const componentValues = {};
|
|
1497
|
+
collectedComponents.forEach(component => {
|
|
1498
|
+
const componentData = fingerprintData[component];
|
|
1499
|
+
if (componentData && componentData.value) {
|
|
1500
|
+
componentValues[component] = componentData.value;
|
|
1501
|
+
}
|
|
1502
|
+
});
|
|
1503
|
+
// Create final fingerprint hash
|
|
1504
|
+
const hash = hashFingerprint(componentValues);
|
|
1505
|
+
// Finalize fingerprint data
|
|
1506
|
+
const finalData = {
|
|
1507
|
+
...fingerprintData,
|
|
1508
|
+
hash,
|
|
1509
|
+
components: collectedComponents,
|
|
1510
|
+
collectionTime: performance.now() - startTime
|
|
1511
|
+
};
|
|
1512
|
+
const result = {
|
|
1513
|
+
success: collectedComponents.length > 0,
|
|
1514
|
+
collectedComponents,
|
|
1515
|
+
failedComponents
|
|
1516
|
+
};
|
|
1517
|
+
if (collectedComponents.length > 0) {
|
|
1518
|
+
result.data = finalData;
|
|
1519
|
+
}
|
|
1520
|
+
else {
|
|
1521
|
+
result.error = 'No components could be collected';
|
|
1522
|
+
}
|
|
1523
|
+
return result;
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Generate visitor ID from fingerprint
|
|
1527
|
+
*/
|
|
1528
|
+
function generateVisitorIdFromFingerprint(fingerprintData) {
|
|
1529
|
+
return generateVisitorId(fingerprintData.hash);
|
|
1530
|
+
}
|
|
1531
|
+
/**
|
|
1532
|
+
* Get a lightweight fingerprint (GDPR-compliant)
|
|
1533
|
+
*/
|
|
1534
|
+
async function getLightweightFingerprint() {
|
|
1535
|
+
return collectFingerprint({
|
|
1536
|
+
gdprMode: true,
|
|
1537
|
+
includeInvasive: false,
|
|
1538
|
+
excludeComponents: ['canvas', 'webgl', 'audio'],
|
|
1539
|
+
timeout: 2000
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Get a complete fingerprint (enhanced mode)
|
|
1544
|
+
*/
|
|
1545
|
+
async function getCompleteFingerprint() {
|
|
1546
|
+
return collectFingerprint({
|
|
1547
|
+
gdprMode: false,
|
|
1548
|
+
includeInvasive: true,
|
|
1549
|
+
timeout: 5000
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1553
|
+
* Clear session fingerprint cache (memory only)
|
|
1554
|
+
*/
|
|
1555
|
+
function clearFingerprintCache() {
|
|
1556
|
+
sessionCache.clear();
|
|
1557
|
+
}
|
|
1558
|
+
/**
|
|
1559
|
+
* Check if fingerprinting is available
|
|
1560
|
+
*/
|
|
1561
|
+
function isFingerprintingAvailable() {
|
|
1562
|
+
// At minimum, we need screen and browser fingerprinting
|
|
1563
|
+
return isScreenAvailable() && isBrowserFingerprintingAvailable();
|
|
1564
|
+
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Get available fingerprinting components
|
|
1567
|
+
*/
|
|
1568
|
+
function getAvailableComponents() {
|
|
1569
|
+
const available = [];
|
|
1570
|
+
if (isCanvasAvailable$1())
|
|
1571
|
+
available.push('canvas');
|
|
1572
|
+
if (isWebGLAvailable())
|
|
1573
|
+
available.push('webgl');
|
|
1574
|
+
if (isAudioAvailable())
|
|
1575
|
+
available.push('audio');
|
|
1576
|
+
if (isFontDetectionAvailable())
|
|
1577
|
+
available.push('fonts');
|
|
1578
|
+
if (isScreenAvailable())
|
|
1579
|
+
available.push('screen');
|
|
1580
|
+
if (isBrowserFingerprintingAvailable())
|
|
1581
|
+
available.push('browser');
|
|
1582
|
+
return available;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
/**
|
|
1586
|
+
* RabbitTracker SDK v3.0.0
|
|
1587
|
+
* 100% Cookieless Analytics Tracking
|
|
1588
|
+
*
|
|
1589
|
+
* Main SDK class for client-side tracking integration
|
|
1590
|
+
*/
|
|
1591
|
+
/**
|
|
1592
|
+
* Default internal configuration
|
|
1593
|
+
*/
|
|
1594
|
+
const DEFAULT_CONFIG = {
|
|
1595
|
+
apiEndpoint: 'https://api.rabbitracker.com',
|
|
1596
|
+
retryEnabled: true,
|
|
1597
|
+
maxRetries: 3,
|
|
1598
|
+
retryDelays: [1000, 2000, 4000],
|
|
1599
|
+
autoTrack: true,
|
|
1600
|
+
dedupeEnabled: true,
|
|
1601
|
+
fingerprintEnabled: true,
|
|
1602
|
+
presenceTracking: false,
|
|
1603
|
+
heartbeatInterval: 30000,
|
|
1604
|
+
scrollTracking: true,
|
|
1605
|
+
scrollDepthTracking: true,
|
|
1606
|
+
gdprCompliant: true,
|
|
1607
|
+
enhancedTracking: 'ask',
|
|
1608
|
+
replaySampling: 0.1,
|
|
1609
|
+
replayMaskInputs: true
|
|
1610
|
+
};
|
|
1611
|
+
/**
|
|
1612
|
+
* Main RabbitTracker SDK Class
|
|
1613
|
+
*/
|
|
1614
|
+
class RabbitTrackerSDK {
|
|
1615
|
+
constructor(userConfig) {
|
|
1616
|
+
this.version = '3.0.0';
|
|
1617
|
+
this.isInitialized = false;
|
|
1618
|
+
this.eventQueue = [];
|
|
1619
|
+
/**
|
|
1620
|
+
* Heatmap API
|
|
1621
|
+
*/
|
|
1622
|
+
this.heatmap = {
|
|
1623
|
+
enable: () => {
|
|
1624
|
+
this.config.heatmap = true;
|
|
1625
|
+
if (this.config.debug) {
|
|
1626
|
+
console.log('[RabbitTracker] Heatmap enabled');
|
|
1627
|
+
}
|
|
1628
|
+
},
|
|
1629
|
+
disable: () => {
|
|
1630
|
+
this.config.heatmap = false;
|
|
1631
|
+
if (this.config.debug) {
|
|
1632
|
+
console.log('[RabbitTracker] Heatmap disabled');
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
};
|
|
1636
|
+
/**
|
|
1637
|
+
* Replay API
|
|
1638
|
+
*/
|
|
1639
|
+
this.replay = {
|
|
1640
|
+
enable: () => {
|
|
1641
|
+
this.config.replay = true;
|
|
1642
|
+
if (this.config.debug) {
|
|
1643
|
+
console.log('[RabbitTracker] Replay enabled');
|
|
1644
|
+
}
|
|
1645
|
+
},
|
|
1646
|
+
disable: () => {
|
|
1647
|
+
this.config.replay = false;
|
|
1648
|
+
if (this.config.debug) {
|
|
1649
|
+
console.log('[RabbitTracker] Replay disabled');
|
|
1650
|
+
}
|
|
1651
|
+
},
|
|
1652
|
+
addFunnelStep: (stepData) => {
|
|
1653
|
+
this.sendEvent({
|
|
1654
|
+
eventType: 'funnel_step',
|
|
1655
|
+
eventName: 'funnel_step',
|
|
1656
|
+
customData: stepData
|
|
1657
|
+
});
|
|
1658
|
+
},
|
|
1659
|
+
markConversion: (data) => {
|
|
1660
|
+
this.trackConversion('funnel_conversion', data.value, data.currency, data);
|
|
1661
|
+
}
|
|
1662
|
+
};
|
|
1663
|
+
// Validate required config
|
|
1664
|
+
if (!userConfig.token) {
|
|
1665
|
+
throw new Error('RabbitTracker: token is required');
|
|
1666
|
+
}
|
|
1667
|
+
// Merge user config with defaults
|
|
1668
|
+
this.config = {
|
|
1669
|
+
...DEFAULT_CONFIG,
|
|
1670
|
+
...userConfig,
|
|
1671
|
+
gdprMode: userConfig.gdprMode ?? false
|
|
1672
|
+
};
|
|
1673
|
+
// Auto-initialize if in browser
|
|
1674
|
+
if (typeof window !== 'undefined') {
|
|
1675
|
+
this.initialize();
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
/**
|
|
1679
|
+
* Initialize SDK
|
|
1680
|
+
*/
|
|
1681
|
+
async initialize() {
|
|
1682
|
+
try {
|
|
1683
|
+
if (this.config.debug) {
|
|
1684
|
+
console.log('[RabbitTracker] Initializing SDK v' + this.version);
|
|
1685
|
+
}
|
|
1686
|
+
// Check localhost restriction
|
|
1687
|
+
if (!this.config.allow_localhost && this.isLocalhost()) {
|
|
1688
|
+
if (this.config.debug) {
|
|
1689
|
+
console.log('[RabbitTracker] Skipping tracking on localhost');
|
|
1690
|
+
}
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
// Collect fingerprint
|
|
1694
|
+
await this.collectFingerprint();
|
|
1695
|
+
// Auto-track page view if enabled
|
|
1696
|
+
if (this.config.autoTrack) {
|
|
1697
|
+
this.trackPageView();
|
|
1698
|
+
}
|
|
1699
|
+
this.isInitialized = true;
|
|
1700
|
+
// Process queued events
|
|
1701
|
+
this.processEventQueue();
|
|
1702
|
+
if (this.config.debug) {
|
|
1703
|
+
console.log('[RabbitTracker] SDK initialized successfully', {
|
|
1704
|
+
visitorId: this.visitorId,
|
|
1705
|
+
gdprMode: this.config.gdprMode
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
catch (error) {
|
|
1710
|
+
console.error('[RabbitTracker] Initialization failed:', error);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* Collect fingerprint and generate visitor ID
|
|
1715
|
+
*/
|
|
1716
|
+
async collectFingerprint() {
|
|
1717
|
+
try {
|
|
1718
|
+
// Use appropriate fingerprinting based on GDPR mode
|
|
1719
|
+
const result = this.config.gdprMode
|
|
1720
|
+
? await getLightweightFingerprint()
|
|
1721
|
+
: await getCompleteFingerprint();
|
|
1722
|
+
if (result.success && result.data) {
|
|
1723
|
+
this.fingerprint = result.data;
|
|
1724
|
+
this.visitorId = generateVisitorIdFromFingerprint(result.data);
|
|
1725
|
+
if (this.config.debug) {
|
|
1726
|
+
console.log('[RabbitTracker] Fingerprint collected:', {
|
|
1727
|
+
components: result.collectedComponents,
|
|
1728
|
+
visitorId: this.visitorId,
|
|
1729
|
+
gdprMode: this.config.gdprMode
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
else {
|
|
1734
|
+
throw new Error('Fingerprint collection failed');
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
catch (error) {
|
|
1738
|
+
if (this.config.debug) {
|
|
1739
|
+
console.error('[RabbitTracker] Fingerprint collection failed:', error);
|
|
1740
|
+
}
|
|
1741
|
+
// Fallback to basic fingerprint
|
|
1742
|
+
this.visitorId = 'fallback_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1746
|
+
* Check if running on localhost
|
|
1747
|
+
*/
|
|
1748
|
+
isLocalhost() {
|
|
1749
|
+
if (typeof window === 'undefined')
|
|
1750
|
+
return false;
|
|
1751
|
+
return window.location.hostname === 'localhost' ||
|
|
1752
|
+
window.location.hostname === '127.0.0.1' ||
|
|
1753
|
+
window.location.hostname.startsWith('192.168.') ||
|
|
1754
|
+
window.location.hostname.startsWith('10.') ||
|
|
1755
|
+
window.location.hostname.includes('local');
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Send event to backend
|
|
1759
|
+
*/
|
|
1760
|
+
async sendEvent(eventData) {
|
|
1761
|
+
if (!this.isInitialized) {
|
|
1762
|
+
this.eventQueue.push(eventData);
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1765
|
+
try {
|
|
1766
|
+
const payload = {
|
|
1767
|
+
workspaceId: this.config.token,
|
|
1768
|
+
fingerprint: this.fingerprint?.hash || this.visitorId,
|
|
1769
|
+
fingerprintComponents: this.fingerprint?.components || {},
|
|
1770
|
+
...eventData,
|
|
1771
|
+
timestamp: new Date().toISOString(),
|
|
1772
|
+
url: window.location.href,
|
|
1773
|
+
referrer: document.referrer,
|
|
1774
|
+
pageTitle: document.title,
|
|
1775
|
+
userAgent: navigator.userAgent,
|
|
1776
|
+
screenWidth: window.screen.width,
|
|
1777
|
+
screenHeight: window.screen.height,
|
|
1778
|
+
viewportWidth: window.innerWidth,
|
|
1779
|
+
viewportHeight: window.innerHeight,
|
|
1780
|
+
gdprMode: this.config.gdprMode
|
|
1781
|
+
};
|
|
1782
|
+
const response = await this.makeRequest('/tracking/event', payload);
|
|
1783
|
+
if (response.sessionId) {
|
|
1784
|
+
this.sessionId = response.sessionId;
|
|
1785
|
+
}
|
|
1786
|
+
return response;
|
|
1787
|
+
}
|
|
1788
|
+
catch (error) {
|
|
1789
|
+
if (this.config.debug) {
|
|
1790
|
+
console.error('[RabbitTracker] Event sending failed:', error);
|
|
1791
|
+
}
|
|
1792
|
+
// Retry logic
|
|
1793
|
+
if (this.config.retryEnabled) {
|
|
1794
|
+
// Queue for retry (simplified)
|
|
1795
|
+
setTimeout(() => this.sendEvent(eventData), 5000);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
/**
|
|
1800
|
+
* Make HTTP request to backend
|
|
1801
|
+
*/
|
|
1802
|
+
async makeRequest(endpoint, data) {
|
|
1803
|
+
const url = `${this.config.apiEndpoint}${endpoint}`;
|
|
1804
|
+
const response = await fetch(url, {
|
|
1805
|
+
method: 'POST',
|
|
1806
|
+
headers: {
|
|
1807
|
+
'Content-Type': 'application/json',
|
|
1808
|
+
},
|
|
1809
|
+
body: JSON.stringify(data)
|
|
1810
|
+
});
|
|
1811
|
+
if (!response.ok) {
|
|
1812
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1813
|
+
}
|
|
1814
|
+
return response.json();
|
|
1815
|
+
}
|
|
1816
|
+
/**
|
|
1817
|
+
* Process queued events
|
|
1818
|
+
*/
|
|
1819
|
+
processEventQueue() {
|
|
1820
|
+
while (this.eventQueue.length > 0) {
|
|
1821
|
+
const event = this.eventQueue.shift();
|
|
1822
|
+
this.sendEvent(event);
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
// Public API Methods
|
|
1826
|
+
/**
|
|
1827
|
+
* Track generic event
|
|
1828
|
+
*/
|
|
1829
|
+
track(eventType, eventData) {
|
|
1830
|
+
this.sendEvent({
|
|
1831
|
+
eventType,
|
|
1832
|
+
eventName: eventType,
|
|
1833
|
+
customData: eventData
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1836
|
+
/**
|
|
1837
|
+
* Track custom event
|
|
1838
|
+
*/
|
|
1839
|
+
trackCustomEvent(eventName, metadata) {
|
|
1840
|
+
this.sendEvent({
|
|
1841
|
+
eventType: 'custom',
|
|
1842
|
+
eventName,
|
|
1843
|
+
customData: metadata
|
|
1844
|
+
});
|
|
1845
|
+
return true;
|
|
1846
|
+
}
|
|
1847
|
+
/**
|
|
1848
|
+
* Track conversion event
|
|
1849
|
+
*/
|
|
1850
|
+
trackConversion(eventName, value, currency, metadata) {
|
|
1851
|
+
this.sendEvent({
|
|
1852
|
+
eventType: 'conversion',
|
|
1853
|
+
eventName,
|
|
1854
|
+
revenue: value,
|
|
1855
|
+
currency: currency || 'USD',
|
|
1856
|
+
customData: metadata
|
|
1857
|
+
});
|
|
1858
|
+
return true;
|
|
1859
|
+
}
|
|
1860
|
+
/**
|
|
1861
|
+
* Track page view
|
|
1862
|
+
*/
|
|
1863
|
+
trackPageView(data) {
|
|
1864
|
+
this.sendEvent({
|
|
1865
|
+
eventType: 'page_view',
|
|
1866
|
+
eventName: 'page_view',
|
|
1867
|
+
...data
|
|
1868
|
+
});
|
|
1869
|
+
}
|
|
1870
|
+
/**
|
|
1871
|
+
* Track purchase
|
|
1872
|
+
*/
|
|
1873
|
+
trackPurchase(data) {
|
|
1874
|
+
this.sendEvent({
|
|
1875
|
+
eventType: 'purchase',
|
|
1876
|
+
eventName: 'purchase',
|
|
1877
|
+
revenue: data.value || data.revenue,
|
|
1878
|
+
currency: data.currency || 'USD',
|
|
1879
|
+
quantity: data.quantity || 1,
|
|
1880
|
+
productId: data.productId,
|
|
1881
|
+
customData: {
|
|
1882
|
+
productName: data.productName,
|
|
1883
|
+
orderId: data.orderId,
|
|
1884
|
+
...data.customData
|
|
1885
|
+
}
|
|
1886
|
+
});
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* Track add to cart
|
|
1890
|
+
*/
|
|
1891
|
+
trackAddToCart(data) {
|
|
1892
|
+
this.sendEvent({
|
|
1893
|
+
eventType: 'add_to_cart',
|
|
1894
|
+
eventName: 'add_to_cart',
|
|
1895
|
+
revenue: data?.price,
|
|
1896
|
+
quantity: data?.quantity || 1,
|
|
1897
|
+
productId: data?.productId,
|
|
1898
|
+
customData: {
|
|
1899
|
+
productName: data?.productName,
|
|
1900
|
+
...data?.customData
|
|
1901
|
+
}
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1904
|
+
/**
|
|
1905
|
+
* Track view content
|
|
1906
|
+
*/
|
|
1907
|
+
trackViewContent(data) {
|
|
1908
|
+
this.sendEvent({
|
|
1909
|
+
eventType: 'view_content',
|
|
1910
|
+
eventName: 'view_content',
|
|
1911
|
+
productId: data?.productId,
|
|
1912
|
+
customData: {
|
|
1913
|
+
productName: data?.productName,
|
|
1914
|
+
category: data?.category,
|
|
1915
|
+
...data?.customData
|
|
1916
|
+
}
|
|
1917
|
+
});
|
|
1918
|
+
}
|
|
1919
|
+
/**
|
|
1920
|
+
* Track initiate checkout
|
|
1921
|
+
*/
|
|
1922
|
+
trackInitiateCheckout(data) {
|
|
1923
|
+
this.sendEvent({
|
|
1924
|
+
eventType: 'initiate_checkout',
|
|
1925
|
+
eventName: 'initiate_checkout',
|
|
1926
|
+
revenue: data?.value,
|
|
1927
|
+
currency: data?.currency || 'USD',
|
|
1928
|
+
quantity: data?.numItems,
|
|
1929
|
+
customData: data?.customData
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
/**
|
|
1933
|
+
* Track lead
|
|
1934
|
+
*/
|
|
1935
|
+
trackLead(data) {
|
|
1936
|
+
this.sendEvent({
|
|
1937
|
+
eventType: 'lead',
|
|
1938
|
+
eventName: 'lead',
|
|
1939
|
+
customData: data
|
|
1940
|
+
});
|
|
1941
|
+
}
|
|
1942
|
+
/**
|
|
1943
|
+
* Identify user
|
|
1944
|
+
*/
|
|
1945
|
+
identify(userData) {
|
|
1946
|
+
this.sendEvent({
|
|
1947
|
+
eventType: 'identify',
|
|
1948
|
+
eventName: 'identify',
|
|
1949
|
+
email: userData.email,
|
|
1950
|
+
phone: userData.phone,
|
|
1951
|
+
customData: userData
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
/**
|
|
1955
|
+
* Track SPA navigation
|
|
1956
|
+
*/
|
|
1957
|
+
trackSPANavigation(url, title) {
|
|
1958
|
+
this.trackPageView({
|
|
1959
|
+
url: url || window.location.href,
|
|
1960
|
+
pageTitle: title || document.title
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
1963
|
+
/**
|
|
1964
|
+
* Reset scroll tracking
|
|
1965
|
+
*/
|
|
1966
|
+
resetScrollTracking() {
|
|
1967
|
+
// Implementation for scroll tracking reset
|
|
1968
|
+
if (this.config.debug) {
|
|
1969
|
+
console.log('[RabbitTracker] Scroll tracking reset');
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
/**
|
|
1973
|
+
* Start presence tracking
|
|
1974
|
+
*/
|
|
1975
|
+
startPresenceTracking() {
|
|
1976
|
+
this.config.presenceTracking = true;
|
|
1977
|
+
if (this.config.debug) {
|
|
1978
|
+
console.log('[RabbitTracker] Presence tracking started');
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
/**
|
|
1982
|
+
* Stop presence tracking
|
|
1983
|
+
*/
|
|
1984
|
+
stopPresenceTracking() {
|
|
1985
|
+
this.config.presenceTracking = false;
|
|
1986
|
+
if (this.config.debug) {
|
|
1987
|
+
console.log('[RabbitTracker] Presence tracking stopped');
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
/**
|
|
1991
|
+
* Enable enhanced tracking
|
|
1992
|
+
*/
|
|
1993
|
+
enableEnhancedTracking() {
|
|
1994
|
+
this.config.enhancedTracking = 'true';
|
|
1995
|
+
this.config.gdprMode = false;
|
|
1996
|
+
// Re-collect fingerprint with enhanced mode
|
|
1997
|
+
this.collectFingerprint();
|
|
1998
|
+
if (this.config.debug) {
|
|
1999
|
+
console.log('[RabbitTracker] Enhanced tracking enabled');
|
|
2000
|
+
}
|
|
2001
|
+
return true;
|
|
2002
|
+
}
|
|
2003
|
+
/**
|
|
2004
|
+
* Disable enhanced tracking
|
|
2005
|
+
*/
|
|
2006
|
+
disableEnhancedTracking() {
|
|
2007
|
+
this.config.enhancedTracking = 'false';
|
|
2008
|
+
this.config.gdprMode = true;
|
|
2009
|
+
if (this.config.debug) {
|
|
2010
|
+
console.log('[RabbitTracker] Enhanced tracking disabled');
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
/**
|
|
2014
|
+
* Check if enhanced mode is active
|
|
2015
|
+
*/
|
|
2016
|
+
isEnhancedMode() {
|
|
2017
|
+
return this.config.enhancedTracking === 'true' && !this.config.gdprMode;
|
|
2018
|
+
}
|
|
2019
|
+
/**
|
|
2020
|
+
* Get user data
|
|
2021
|
+
*/
|
|
2022
|
+
getUserData() {
|
|
2023
|
+
return {
|
|
2024
|
+
sessionId: this.sessionId || '',
|
|
2025
|
+
location: {}, // Will be populated by backend
|
|
2026
|
+
device: {
|
|
2027
|
+
userAgent: navigator.userAgent,
|
|
2028
|
+
language: navigator.language,
|
|
2029
|
+
screen: {
|
|
2030
|
+
width: window.screen.width,
|
|
2031
|
+
height: window.screen.height
|
|
2032
|
+
}
|
|
2033
|
+
},
|
|
2034
|
+
journey: [], // Will be populated by backend
|
|
2035
|
+
fingerprint: this.fingerprint?.hash
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
2038
|
+
/**
|
|
2039
|
+
* Get conversion likelihood (mock implementation)
|
|
2040
|
+
*/
|
|
2041
|
+
getConversionLikelihood() {
|
|
2042
|
+
return {
|
|
2043
|
+
score: 0.5,
|
|
2044
|
+
factors: ['Requires backend analysis']
|
|
2045
|
+
};
|
|
2046
|
+
}
|
|
2047
|
+
/**
|
|
2048
|
+
* Get user segment (mock implementation)
|
|
2049
|
+
*/
|
|
2050
|
+
getUserSegment() {
|
|
2051
|
+
return {
|
|
2052
|
+
type: 'medium_intent',
|
|
2053
|
+
confidence: 0.5
|
|
2054
|
+
};
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
/**
|
|
2059
|
+
* Bot Detection Module
|
|
2060
|
+
* Comprehensive client-side bot detection based on multiple techniques
|
|
2061
|
+
* Detects headless browsers, automation tools, and suspicious behavior
|
|
2062
|
+
*/
|
|
2063
|
+
/**
|
|
2064
|
+
* Known bot user agent patterns
|
|
2065
|
+
*/
|
|
2066
|
+
const BOT_PATTERNS = [
|
|
2067
|
+
// Headless browsers
|
|
2068
|
+
/HeadlessChrome/i,
|
|
2069
|
+
/PhantomJS/i,
|
|
2070
|
+
/SlimerJS/i,
|
|
2071
|
+
// Automation tools
|
|
2072
|
+
/Selenium/i,
|
|
2073
|
+
/WebDriver/i,
|
|
2074
|
+
/ChromeDriver/i,
|
|
2075
|
+
/GeckoDriver/i,
|
|
2076
|
+
// Crawlers and bots
|
|
2077
|
+
/bot/i,
|
|
2078
|
+
/crawler/i,
|
|
2079
|
+
/spider/i,
|
|
2080
|
+
/scraper/i,
|
|
2081
|
+
// Specific bots
|
|
2082
|
+
/Googlebot/i,
|
|
2083
|
+
/Bingbot/i,
|
|
2084
|
+
/Slurp/i,
|
|
2085
|
+
/DuckDuckBot/i,
|
|
2086
|
+
/Baiduspider/i,
|
|
2087
|
+
/YandexBot/i,
|
|
2088
|
+
/facebookexternalhit/i,
|
|
2089
|
+
/Twitterbot/i,
|
|
2090
|
+
/LinkedInBot/i,
|
|
2091
|
+
/WhatsApp/i,
|
|
2092
|
+
/SkypeUriPreview/i
|
|
2093
|
+
];
|
|
2094
|
+
/**
|
|
2095
|
+
* Suspicious user agent patterns
|
|
2096
|
+
*/
|
|
2097
|
+
const SUSPICIOUS_PATTERNS = [
|
|
2098
|
+
/^Mozilla\/5\.0$/,
|
|
2099
|
+
/Headless/i,
|
|
2100
|
+
/automated/i,
|
|
2101
|
+
/testing/i,
|
|
2102
|
+
/^$/
|
|
2103
|
+
];
|
|
2104
|
+
/**
|
|
2105
|
+
* Detect WebDriver presence
|
|
2106
|
+
*/
|
|
2107
|
+
function detectWebDriver() {
|
|
2108
|
+
try {
|
|
2109
|
+
// Check for webdriver property
|
|
2110
|
+
if (navigator.webdriver === true) {
|
|
2111
|
+
return {
|
|
2112
|
+
detected: true,
|
|
2113
|
+
confidence: 95,
|
|
2114
|
+
method: 'navigator.webdriver_true'
|
|
2115
|
+
};
|
|
2116
|
+
}
|
|
2117
|
+
// Check for webdriver in window
|
|
2118
|
+
if (window.webdriver === true) {
|
|
2119
|
+
return {
|
|
2120
|
+
detected: true,
|
|
2121
|
+
confidence: 90,
|
|
2122
|
+
method: 'window.webdriver_true'
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
// Check for selenium-specific properties
|
|
2126
|
+
const seleniumKeys = [
|
|
2127
|
+
'_selenium',
|
|
2128
|
+
'callSelenium',
|
|
2129
|
+
'_Selenium_IDE_Recorder',
|
|
2130
|
+
'callPhantom',
|
|
2131
|
+
'_phantom',
|
|
2132
|
+
'__phantomas',
|
|
2133
|
+
'__fxdriver_evaluate',
|
|
2134
|
+
'__fxdriver_unwrapped',
|
|
2135
|
+
'_fxdriver_evaluate',
|
|
2136
|
+
'_fxdriver_unwrapped'
|
|
2137
|
+
];
|
|
2138
|
+
for (const key of seleniumKeys) {
|
|
2139
|
+
if (key in window || key in document) {
|
|
2140
|
+
return {
|
|
2141
|
+
detected: true,
|
|
2142
|
+
confidence: 85,
|
|
2143
|
+
method: `selenium_property_${key}`
|
|
2144
|
+
};
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
// Check for webdriver in document
|
|
2148
|
+
if (document.documentElement.getAttribute('webdriver')) {
|
|
2149
|
+
return {
|
|
2150
|
+
detected: true,
|
|
2151
|
+
confidence: 80,
|
|
2152
|
+
method: 'document_webdriver_attribute'
|
|
2153
|
+
};
|
|
2154
|
+
}
|
|
2155
|
+
return {
|
|
2156
|
+
detected: false,
|
|
2157
|
+
confidence: 0,
|
|
2158
|
+
method: 'webdriver_not_detected'
|
|
2159
|
+
};
|
|
2160
|
+
}
|
|
2161
|
+
catch (error) {
|
|
2162
|
+
return {
|
|
2163
|
+
detected: false,
|
|
2164
|
+
confidence: 0,
|
|
2165
|
+
method: 'webdriver_error',
|
|
2166
|
+
details: { error: error instanceof Error ? error.message : 'Unknown' }
|
|
2167
|
+
};
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
/**
|
|
2171
|
+
* Detect headless browser characteristics
|
|
2172
|
+
*/
|
|
2173
|
+
function detectHeadlessBrowser() {
|
|
2174
|
+
try {
|
|
2175
|
+
let suspiciousCount = 0;
|
|
2176
|
+
const issues = [];
|
|
2177
|
+
// Check for missing window properties
|
|
2178
|
+
if (!window.outerHeight || !window.outerWidth) {
|
|
2179
|
+
suspiciousCount++;
|
|
2180
|
+
issues.push('missing_outer_dimensions');
|
|
2181
|
+
}
|
|
2182
|
+
// Check for plugins
|
|
2183
|
+
if (navigator.plugins.length === 0) {
|
|
2184
|
+
suspiciousCount++;
|
|
2185
|
+
issues.push('no_plugins');
|
|
2186
|
+
}
|
|
2187
|
+
// Check for languages
|
|
2188
|
+
if (!navigator.languages || navigator.languages.length === 0) {
|
|
2189
|
+
suspiciousCount++;
|
|
2190
|
+
issues.push('no_languages');
|
|
2191
|
+
}
|
|
2192
|
+
// Check for inconsistent user agent
|
|
2193
|
+
const userAgent = navigator.userAgent;
|
|
2194
|
+
if (!userAgent || userAgent.length < 50) {
|
|
2195
|
+
suspiciousCount++;
|
|
2196
|
+
issues.push('short_user_agent');
|
|
2197
|
+
}
|
|
2198
|
+
// Check for notification permission (often undefined in headless)
|
|
2199
|
+
if (typeof Notification?.permission === 'undefined') {
|
|
2200
|
+
suspiciousCount++;
|
|
2201
|
+
issues.push('no_notification_permission');
|
|
2202
|
+
}
|
|
2203
|
+
// Check for missing performance API
|
|
2204
|
+
if (!window.performance || !window.performance.timing) {
|
|
2205
|
+
suspiciousCount++;
|
|
2206
|
+
issues.push('missing_performance_api');
|
|
2207
|
+
}
|
|
2208
|
+
// Check Chrome-specific headless indicators
|
|
2209
|
+
if (userAgent.includes('Chrome')) {
|
|
2210
|
+
// Check for missing chrome object
|
|
2211
|
+
if (!window.chrome) {
|
|
2212
|
+
suspiciousCount++;
|
|
2213
|
+
issues.push('missing_chrome_object');
|
|
2214
|
+
}
|
|
2215
|
+
// Check for headless in user agent
|
|
2216
|
+
if (userAgent.includes('HeadlessChrome')) {
|
|
2217
|
+
return {
|
|
2218
|
+
detected: true,
|
|
2219
|
+
confidence: 95,
|
|
2220
|
+
method: 'chrome_headless_user_agent'
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
const confidence = Math.min(suspiciousCount * 15, 90);
|
|
2225
|
+
return {
|
|
2226
|
+
detected: suspiciousCount >= 3,
|
|
2227
|
+
confidence,
|
|
2228
|
+
method: 'headless_characteristics',
|
|
2229
|
+
details: { issues, suspiciousCount }
|
|
2230
|
+
};
|
|
2231
|
+
}
|
|
2232
|
+
catch (error) {
|
|
2233
|
+
return {
|
|
2234
|
+
detected: false,
|
|
2235
|
+
confidence: 0,
|
|
2236
|
+
method: 'headless_detection_error',
|
|
2237
|
+
details: { error: error instanceof Error ? error.message : 'Unknown' }
|
|
2238
|
+
};
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
/**
|
|
2242
|
+
* Detect automation tools
|
|
2243
|
+
*/
|
|
2244
|
+
function detectAutomationTools() {
|
|
2245
|
+
try {
|
|
2246
|
+
const automationIndicators = [
|
|
2247
|
+
'webdriver',
|
|
2248
|
+
'selenium',
|
|
2249
|
+
'phantomjs',
|
|
2250
|
+
'slimerjs',
|
|
2251
|
+
'chromedriver',
|
|
2252
|
+
'geckodriver',
|
|
2253
|
+
'automation',
|
|
2254
|
+
'puppeteer'
|
|
2255
|
+
];
|
|
2256
|
+
const detectedTools = [];
|
|
2257
|
+
// Check window properties
|
|
2258
|
+
for (const tool of automationIndicators) {
|
|
2259
|
+
if (window[tool] || window[`_${tool}`] || window[`__${tool}`]) {
|
|
2260
|
+
detectedTools.push(tool);
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
// Check document properties
|
|
2264
|
+
for (const tool of automationIndicators) {
|
|
2265
|
+
if (document[tool] || document[`_${tool}`] || document[`__${tool}`]) {
|
|
2266
|
+
detectedTools.push(tool);
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
// Check for automation-specific errors
|
|
2270
|
+
try {
|
|
2271
|
+
// Use safer method to test navigation object access
|
|
2272
|
+
const nav = window.navigator;
|
|
2273
|
+
if (!nav) {
|
|
2274
|
+
detectedTools.push('navigation_missing');
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
catch (error) {
|
|
2278
|
+
const errorMessage = error instanceof Error ? error.message : '';
|
|
2279
|
+
if (errorMessage.includes('automation') || errorMessage.includes('webdriver')) {
|
|
2280
|
+
detectedTools.push('navigation_automation_error');
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
if (detectedTools.length > 0) {
|
|
2284
|
+
return {
|
|
2285
|
+
detected: true,
|
|
2286
|
+
confidence: Math.min(detectedTools.length * 30, 95),
|
|
2287
|
+
method: 'automation_tools_detected',
|
|
2288
|
+
details: { tools: detectedTools }
|
|
2289
|
+
};
|
|
2290
|
+
}
|
|
2291
|
+
return {
|
|
2292
|
+
detected: false,
|
|
2293
|
+
confidence: 0,
|
|
2294
|
+
method: 'no_automation_tools'
|
|
2295
|
+
};
|
|
2296
|
+
}
|
|
2297
|
+
catch (error) {
|
|
2298
|
+
return {
|
|
2299
|
+
detected: false,
|
|
2300
|
+
confidence: 0,
|
|
2301
|
+
method: 'automation_detection_error',
|
|
2302
|
+
details: { error: error instanceof Error ? error.message : 'Unknown' }
|
|
2303
|
+
};
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
/**
|
|
2307
|
+
* Detect DOM blockers (ad blockers and privacy tools)
|
|
2308
|
+
*/
|
|
2309
|
+
function detectDOMBlockers() {
|
|
2310
|
+
try {
|
|
2311
|
+
const blockerSelectors = [
|
|
2312
|
+
// Ad blocker elements
|
|
2313
|
+
'#ad-banner',
|
|
2314
|
+
'.advertisement',
|
|
2315
|
+
'.google-ads',
|
|
2316
|
+
'.adsense',
|
|
2317
|
+
// Analytics blockers
|
|
2318
|
+
'#google-analytics',
|
|
2319
|
+
'.analytics',
|
|
2320
|
+
'[src*="google-analytics"]',
|
|
2321
|
+
'[src*="googletagmanager"]',
|
|
2322
|
+
// Social media blockers
|
|
2323
|
+
'.facebook-widget',
|
|
2324
|
+
'.twitter-widget',
|
|
2325
|
+
'.social-share'
|
|
2326
|
+
];
|
|
2327
|
+
let blockedCount = 0;
|
|
2328
|
+
for (const selector of blockerSelectors) {
|
|
2329
|
+
const element = document.querySelector(selector);
|
|
2330
|
+
if (!element) {
|
|
2331
|
+
blockedCount++;
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
// Test if we can create tracking pixels
|
|
2335
|
+
try {
|
|
2336
|
+
const pixel = document.createElement('img');
|
|
2337
|
+
pixel.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
|
2338
|
+
pixel.style.display = 'none';
|
|
2339
|
+
document.body.appendChild(pixel);
|
|
2340
|
+
setTimeout(() => {
|
|
2341
|
+
if (pixel.parentNode) {
|
|
2342
|
+
pixel.parentNode.removeChild(pixel);
|
|
2343
|
+
}
|
|
2344
|
+
}, 100);
|
|
2345
|
+
}
|
|
2346
|
+
catch {
|
|
2347
|
+
blockedCount++;
|
|
2348
|
+
}
|
|
2349
|
+
const confidence = Math.min(blockedCount * 20, 80);
|
|
2350
|
+
return {
|
|
2351
|
+
detected: blockedCount >= 2,
|
|
2352
|
+
confidence,
|
|
2353
|
+
method: 'dom_blockers',
|
|
2354
|
+
details: { blockedCount, totalTests: blockerSelectors.length + 1 }
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
catch (error) {
|
|
2358
|
+
return {
|
|
2359
|
+
detected: false,
|
|
2360
|
+
confidence: 0,
|
|
2361
|
+
method: 'dom_blocker_error',
|
|
2362
|
+
details: { error: error instanceof Error ? error.message : 'Unknown' }
|
|
2363
|
+
};
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
/**
|
|
2367
|
+
* Check for missing languages (common in bots)
|
|
2368
|
+
*/
|
|
2369
|
+
function detectMissingLanguages() {
|
|
2370
|
+
try {
|
|
2371
|
+
if (!navigator.languages || navigator.languages.length === 0) {
|
|
2372
|
+
return {
|
|
2373
|
+
detected: true,
|
|
2374
|
+
confidence: 70,
|
|
2375
|
+
method: 'no_languages'
|
|
2376
|
+
};
|
|
2377
|
+
}
|
|
2378
|
+
if (navigator.languages.length === 1 && navigator.languages[0] === 'en-US') {
|
|
2379
|
+
return {
|
|
2380
|
+
detected: true,
|
|
2381
|
+
confidence: 40,
|
|
2382
|
+
method: 'single_default_language'
|
|
2383
|
+
};
|
|
2384
|
+
}
|
|
2385
|
+
// Check for inconsistent language settings
|
|
2386
|
+
if (navigator.language !== navigator.languages[0]) {
|
|
2387
|
+
return {
|
|
2388
|
+
detected: true,
|
|
2389
|
+
confidence: 30,
|
|
2390
|
+
method: 'inconsistent_languages',
|
|
2391
|
+
details: {
|
|
2392
|
+
language: navigator.language,
|
|
2393
|
+
firstLanguage: navigator.languages[0]
|
|
2394
|
+
}
|
|
2395
|
+
};
|
|
2396
|
+
}
|
|
2397
|
+
return {
|
|
2398
|
+
detected: false,
|
|
2399
|
+
confidence: 0,
|
|
2400
|
+
method: 'languages_normal',
|
|
2401
|
+
details: { languageCount: navigator.languages.length }
|
|
2402
|
+
};
|
|
2403
|
+
}
|
|
2404
|
+
catch (error) {
|
|
2405
|
+
return {
|
|
2406
|
+
detected: true,
|
|
2407
|
+
confidence: 50,
|
|
2408
|
+
method: 'language_detection_error',
|
|
2409
|
+
details: { error: error instanceof Error ? error.message : 'Unknown' }
|
|
2410
|
+
};
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
/**
|
|
2414
|
+
* Test for inconsistent eval behavior
|
|
2415
|
+
*/
|
|
2416
|
+
function detectInconsistentEval() {
|
|
2417
|
+
try {
|
|
2418
|
+
// Test if basic math computation works normally
|
|
2419
|
+
const testFunction = new Function('return 2 + 2');
|
|
2420
|
+
const result = testFunction();
|
|
2421
|
+
if (result !== 4) {
|
|
2422
|
+
return {
|
|
2423
|
+
detected: true,
|
|
2424
|
+
confidence: 90,
|
|
2425
|
+
method: 'function_constructor_incorrect_result',
|
|
2426
|
+
details: { expected: 4, actual: result }
|
|
2427
|
+
};
|
|
2428
|
+
}
|
|
2429
|
+
// Test if Function constructor toString() is modified
|
|
2430
|
+
const functionString = Function.toString();
|
|
2431
|
+
if (!functionString.includes('[native code]') && !functionString.includes('function Function()')) {
|
|
2432
|
+
return {
|
|
2433
|
+
detected: true,
|
|
2434
|
+
confidence: 70,
|
|
2435
|
+
method: 'function_constructor_modified_toString',
|
|
2436
|
+
details: { functionString: functionString.substring(0, 100) }
|
|
2437
|
+
};
|
|
2438
|
+
}
|
|
2439
|
+
return {
|
|
2440
|
+
detected: false,
|
|
2441
|
+
confidence: 0,
|
|
2442
|
+
method: 'eval_normal'
|
|
2443
|
+
};
|
|
2444
|
+
}
|
|
2445
|
+
catch (error) {
|
|
2446
|
+
return {
|
|
2447
|
+
detected: true,
|
|
2448
|
+
confidence: 60,
|
|
2449
|
+
method: 'eval_error',
|
|
2450
|
+
details: { error: error instanceof Error ? error.message : 'Unknown' }
|
|
2451
|
+
};
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
/**
|
|
2455
|
+
* Analyze user agent for bot patterns
|
|
2456
|
+
*/
|
|
2457
|
+
function analyzeUserAgent$1() {
|
|
2458
|
+
const userAgent = navigator.userAgent;
|
|
2459
|
+
// Check for bot patterns
|
|
2460
|
+
const botPatterns = BOT_PATTERNS.filter(pattern => pattern.test(userAgent));
|
|
2461
|
+
const suspiciousPatterns = SUSPICIOUS_PATTERNS.filter(pattern => pattern.test(userAgent));
|
|
2462
|
+
let category = 'legitimate';
|
|
2463
|
+
if (botPatterns.some(p => p.source.includes('bot|crawler|spider'))) {
|
|
2464
|
+
category = 'crawler';
|
|
2465
|
+
}
|
|
2466
|
+
else if (botPatterns.some(p => p.source.includes('Selenium|WebDriver'))) {
|
|
2467
|
+
category = 'automation';
|
|
2468
|
+
}
|
|
2469
|
+
else if (botPatterns.some(p => p.source.includes('Headless|Phantom'))) {
|
|
2470
|
+
category = 'headless';
|
|
2471
|
+
}
|
|
2472
|
+
const allPatterns = [...botPatterns.map(p => p.source), ...suspiciousPatterns.map(p => p.source)];
|
|
2473
|
+
return {
|
|
2474
|
+
browser: {
|
|
2475
|
+
name: 'Unknown',
|
|
2476
|
+
version: 'Unknown',
|
|
2477
|
+
major: 'Unknown',
|
|
2478
|
+
engine: 'unknown'
|
|
2479
|
+
},
|
|
2480
|
+
os: {
|
|
2481
|
+
name: 'Unknown',
|
|
2482
|
+
version: 'Unknown',
|
|
2483
|
+
family: 'unknown'
|
|
2484
|
+
},
|
|
2485
|
+
device: {
|
|
2486
|
+
type: 'desktop'
|
|
2487
|
+
},
|
|
2488
|
+
suspicious: {
|
|
2489
|
+
isBot: botPatterns.length > 0 || suspiciousPatterns.length > 0,
|
|
2490
|
+
patterns: allPatterns,
|
|
2491
|
+
category: botPatterns.length > 0 ? category : 'legitimate'
|
|
2492
|
+
},
|
|
2493
|
+
isMobile: false,
|
|
2494
|
+
isTablet: false,
|
|
2495
|
+
isDesktop: true,
|
|
2496
|
+
isBot: botPatterns.length > 0 || suspiciousPatterns.length > 0,
|
|
2497
|
+
raw: userAgent
|
|
2498
|
+
};
|
|
2499
|
+
}
|
|
2500
|
+
/**
|
|
2501
|
+
* Main bot detection function
|
|
2502
|
+
*/
|
|
2503
|
+
async function detectBot() {
|
|
2504
|
+
// Run all detection methods
|
|
2505
|
+
const detectors = {
|
|
2506
|
+
webDriver: detectWebDriver(),
|
|
2507
|
+
headlessBrowser: detectHeadlessBrowser(),
|
|
2508
|
+
automationTools: detectAutomationTools(),
|
|
2509
|
+
domBlockers: detectDOMBlockers(),
|
|
2510
|
+
missingLanguages: detectMissingLanguages(),
|
|
2511
|
+
inconsistentEval: detectInconsistentEval(),
|
|
2512
|
+
// Behavioral detection (placeholder - would need more time to implement)
|
|
2513
|
+
humanBehavior: { detected: false, confidence: 0, method: 'not_implemented' },
|
|
2514
|
+
mouseMovement: { detected: false, confidence: 0, method: 'not_implemented' },
|
|
2515
|
+
clickPatterns: { detected: false, confidence: 0, method: 'not_implemented' },
|
|
2516
|
+
// Canvas/WebGL consistency (placeholder - would integrate with fingerprinting)
|
|
2517
|
+
canvasInconsistency: { detected: false, confidence: 0, method: 'not_implemented' },
|
|
2518
|
+
webglAnomaly: { detected: false, confidence: 0, method: 'not_implemented' },
|
|
2519
|
+
// Plugin detection
|
|
2520
|
+
missingPlugins: { detected: navigator.plugins.length === 0, confidence: 30, method: 'plugin_count' },
|
|
2521
|
+
inconsistentProperties: { detected: false, confidence: 0, method: 'not_implemented' },
|
|
2522
|
+
// Modified built-ins detection
|
|
2523
|
+
modifiedBuiltins: { detected: false, confidence: 0, method: 'not_implemented' }
|
|
2524
|
+
};
|
|
2525
|
+
// Analyze user agent
|
|
2526
|
+
const userAgent = analyzeUserAgent$1();
|
|
2527
|
+
// Calculate overall bot detection
|
|
2528
|
+
const positiveDetections = Object.values(detectors).filter(d => d.detected);
|
|
2529
|
+
const totalConfidence = positiveDetections.reduce((sum, d) => sum + d.confidence, 0);
|
|
2530
|
+
const averageConfidence = positiveDetections.length > 0 ? totalConfidence / positiveDetections.length : 0;
|
|
2531
|
+
// Determine if bot is detected
|
|
2532
|
+
const isBot = positiveDetections.length >= 2 || // Multiple detections
|
|
2533
|
+
positiveDetections.some(d => d.confidence >= 90) || // High confidence detection
|
|
2534
|
+
(userAgent.suspicious?.isBot ?? false); // User agent indicates bot
|
|
2535
|
+
// Determine risk level
|
|
2536
|
+
let riskLevel = 'low';
|
|
2537
|
+
if (averageConfidence >= 70) {
|
|
2538
|
+
riskLevel = 'high';
|
|
2539
|
+
}
|
|
2540
|
+
else if (averageConfidence >= 40 || positiveDetections.length >= 2) {
|
|
2541
|
+
riskLevel = 'medium';
|
|
2542
|
+
}
|
|
2543
|
+
return {
|
|
2544
|
+
isBot,
|
|
2545
|
+
confidence: Math.round(Math.min(averageConfidence, 95)),
|
|
2546
|
+
riskLevel,
|
|
2547
|
+
detectionMethods: Object.keys(detectors).filter(key => detectors[key]?.detected),
|
|
2548
|
+
detectors,
|
|
2549
|
+
userAgent
|
|
2550
|
+
};
|
|
2551
|
+
}
|
|
2552
|
+
/**
|
|
2553
|
+
* Quick bot detection (faster, less comprehensive)
|
|
2554
|
+
*/
|
|
2555
|
+
async function quickBotDetection() {
|
|
2556
|
+
// Check only the most reliable indicators
|
|
2557
|
+
const webDriver = detectWebDriver();
|
|
2558
|
+
const userAgent = analyzeUserAgent$1();
|
|
2559
|
+
return webDriver.detected || (userAgent.suspicious?.isBot ?? false);
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
/**
|
|
2563
|
+
* Incognito Mode Detection (Analytics Only)
|
|
2564
|
+
*
|
|
2565
|
+
* Detects private/incognito browsing mode for analytics purposes only.
|
|
2566
|
+
* Does NOT use localStorage/sessionStorage for tracking or data persistence.
|
|
2567
|
+
* Used only to enrich analytics data with browser context information.
|
|
2568
|
+
*/
|
|
2569
|
+
/**
|
|
2570
|
+
* Test localStorage availability and behavior (ANALYTICS ONLY)
|
|
2571
|
+
*
|
|
2572
|
+
* NOTE: This function uses localStorage purely for DETECTION purposes.
|
|
2573
|
+
* It does NOT store any tracking data or persistent information.
|
|
2574
|
+
* Used only to analyze browser capabilities for analytics context.
|
|
2575
|
+
*/
|
|
2576
|
+
async function testLocalStorage() {
|
|
2577
|
+
try {
|
|
2578
|
+
const testKey = '__incognito_test_' + Date.now();
|
|
2579
|
+
const testValue = 'test';
|
|
2580
|
+
// Try to set and get localStorage item (for detection only, immediately cleaned up)
|
|
2581
|
+
localStorage.setItem(testKey, testValue);
|
|
2582
|
+
const retrieved = localStorage.getItem(testKey);
|
|
2583
|
+
localStorage.removeItem(testKey); // Immediately cleanup test data
|
|
2584
|
+
// In some incognito modes, localStorage works but has limited capacity
|
|
2585
|
+
if (retrieved !== testValue) {
|
|
2586
|
+
return {
|
|
2587
|
+
detected: true,
|
|
2588
|
+
confidence: 80,
|
|
2589
|
+
method: 'localStorage_inconsistency',
|
|
2590
|
+
details: { retrieved, expected: testValue }
|
|
2591
|
+
};
|
|
2592
|
+
}
|
|
2593
|
+
// Test storage capacity (incognito often has reduced limits) - DETECTION ONLY
|
|
2594
|
+
try {
|
|
2595
|
+
const largData = 'x'.repeat(1024 * 1024); // 1MB
|
|
2596
|
+
localStorage.setItem('__capacity_test', largData); // Test only, immediately removed
|
|
2597
|
+
localStorage.removeItem('__capacity_test'); // Immediate cleanup
|
|
2598
|
+
return {
|
|
2599
|
+
detected: false,
|
|
2600
|
+
confidence: 30,
|
|
2601
|
+
method: 'localStorage_capacity'
|
|
2602
|
+
};
|
|
2603
|
+
}
|
|
2604
|
+
catch (capacityError) {
|
|
2605
|
+
// Limited capacity might indicate incognito
|
|
2606
|
+
return {
|
|
2607
|
+
detected: true,
|
|
2608
|
+
confidence: 60,
|
|
2609
|
+
method: 'localStorage_capacity_limited',
|
|
2610
|
+
details: { error: capacityError instanceof Error ? capacityError.message : 'Unknown' }
|
|
2611
|
+
};
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
catch (error) {
|
|
2615
|
+
// localStorage completely unavailable
|
|
2616
|
+
return {
|
|
2617
|
+
detected: true,
|
|
2618
|
+
confidence: 90,
|
|
2619
|
+
method: 'localStorage_unavailable',
|
|
2620
|
+
details: { error: error instanceof Error ? error.message : 'Unknown' }
|
|
2621
|
+
};
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
/**
|
|
2625
|
+
* Test sessionStorage availability (ANALYTICS ONLY)
|
|
2626
|
+
*
|
|
2627
|
+
* NOTE: Uses sessionStorage purely for browser capability detection.
|
|
2628
|
+
* No tracking data is stored - all test data is immediately removed.
|
|
2629
|
+
*/
|
|
2630
|
+
async function testSessionStorage() {
|
|
2631
|
+
try {
|
|
2632
|
+
const testKey = '__session_test_' + Date.now();
|
|
2633
|
+
const testValue = 'test';
|
|
2634
|
+
sessionStorage.setItem(testKey, testValue); // Test only, immediately cleaned
|
|
2635
|
+
const retrieved = sessionStorage.getItem(testKey);
|
|
2636
|
+
sessionStorage.removeItem(testKey); // Immediate cleanup
|
|
2637
|
+
if (retrieved !== testValue) {
|
|
2638
|
+
return {
|
|
2639
|
+
detected: true,
|
|
2640
|
+
confidence: 75,
|
|
2641
|
+
method: 'sessionStorage_inconsistency',
|
|
2642
|
+
details: { retrieved, expected: testValue }
|
|
2643
|
+
};
|
|
2644
|
+
}
|
|
2645
|
+
return {
|
|
2646
|
+
detected: false,
|
|
2647
|
+
confidence: 20,
|
|
2648
|
+
method: 'sessionStorage_working'
|
|
2649
|
+
};
|
|
2650
|
+
}
|
|
2651
|
+
catch (error) {
|
|
2652
|
+
return {
|
|
2653
|
+
detected: true,
|
|
2654
|
+
confidence: 85,
|
|
2655
|
+
method: 'sessionStorage_unavailable',
|
|
2656
|
+
details: { error: error instanceof Error ? error.message : 'Unknown' }
|
|
2657
|
+
};
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
/**
|
|
2661
|
+
* Test IndexedDB availability and behavior
|
|
2662
|
+
*/
|
|
2663
|
+
async function testIndexedDB() {
|
|
2664
|
+
try {
|
|
2665
|
+
if (!window.indexedDB) {
|
|
2666
|
+
return {
|
|
2667
|
+
detected: true,
|
|
2668
|
+
confidence: 70,
|
|
2669
|
+
method: 'indexedDB_unavailable'
|
|
2670
|
+
};
|
|
2671
|
+
}
|
|
2672
|
+
return new Promise((resolve) => {
|
|
2673
|
+
const dbName = '__incognito_test_' + Date.now();
|
|
2674
|
+
const request = indexedDB.open(dbName, 1);
|
|
2675
|
+
const timeout = setTimeout(() => {
|
|
2676
|
+
resolve({
|
|
2677
|
+
detected: true,
|
|
2678
|
+
confidence: 60,
|
|
2679
|
+
method: 'indexedDB_timeout'
|
|
2680
|
+
});
|
|
2681
|
+
}, 1000);
|
|
2682
|
+
request.onerror = () => {
|
|
2683
|
+
clearTimeout(timeout);
|
|
2684
|
+
resolve({
|
|
2685
|
+
detected: true,
|
|
2686
|
+
confidence: 80,
|
|
2687
|
+
method: 'indexedDB_error',
|
|
2688
|
+
details: { error: request.error?.message }
|
|
2689
|
+
});
|
|
2690
|
+
};
|
|
2691
|
+
request.onsuccess = () => {
|
|
2692
|
+
clearTimeout(timeout);
|
|
2693
|
+
const db = request.result;
|
|
2694
|
+
// Clean up
|
|
2695
|
+
db.close();
|
|
2696
|
+
indexedDB.deleteDatabase(dbName);
|
|
2697
|
+
resolve({
|
|
2698
|
+
detected: false,
|
|
2699
|
+
confidence: 25,
|
|
2700
|
+
method: 'indexedDB_working'
|
|
2701
|
+
});
|
|
2702
|
+
};
|
|
2703
|
+
request.onblocked = () => {
|
|
2704
|
+
clearTimeout(timeout);
|
|
2705
|
+
resolve({
|
|
2706
|
+
detected: true,
|
|
2707
|
+
confidence: 70,
|
|
2708
|
+
method: 'indexedDB_blocked'
|
|
2709
|
+
});
|
|
2710
|
+
};
|
|
2711
|
+
});
|
|
2712
|
+
}
|
|
2713
|
+
catch (error) {
|
|
2714
|
+
return {
|
|
2715
|
+
detected: true,
|
|
2716
|
+
confidence: 85,
|
|
2717
|
+
method: 'indexedDB_exception',
|
|
2718
|
+
details: { error: error instanceof Error ? error.message : 'Unknown' }
|
|
2719
|
+
};
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
/**
|
|
2723
|
+
* Test WebSQL availability (legacy browsers)
|
|
2724
|
+
*/
|
|
2725
|
+
async function testOpenDatabase() {
|
|
2726
|
+
try {
|
|
2727
|
+
const openDatabase = window.openDatabase;
|
|
2728
|
+
if (!openDatabase) {
|
|
2729
|
+
return {
|
|
2730
|
+
detected: false,
|
|
2731
|
+
confidence: 10,
|
|
2732
|
+
method: 'openDatabase_unsupported'
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2735
|
+
const db = openDatabase('__test', '1.0', 'Test', 1024);
|
|
2736
|
+
if (!db) {
|
|
2737
|
+
return {
|
|
2738
|
+
detected: true,
|
|
2739
|
+
confidence: 60,
|
|
2740
|
+
method: 'openDatabase_failed'
|
|
2741
|
+
};
|
|
2742
|
+
}
|
|
2743
|
+
return {
|
|
2744
|
+
detected: false,
|
|
2745
|
+
confidence: 20,
|
|
2746
|
+
method: 'openDatabase_working'
|
|
2747
|
+
};
|
|
2748
|
+
}
|
|
2749
|
+
catch (error) {
|
|
2750
|
+
return {
|
|
2751
|
+
detected: true,
|
|
2752
|
+
confidence: 70,
|
|
2753
|
+
method: 'openDatabase_error',
|
|
2754
|
+
details: { error: error instanceof Error ? error.message : 'Unknown' }
|
|
2755
|
+
};
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
/**
|
|
2759
|
+
* Test permissions API behavior
|
|
2760
|
+
*/
|
|
2761
|
+
async function testPermissions() {
|
|
2762
|
+
try {
|
|
2763
|
+
if (!navigator.permissions) {
|
|
2764
|
+
return {
|
|
2765
|
+
detected: false,
|
|
2766
|
+
confidence: 10,
|
|
2767
|
+
method: 'permissions_unsupported'
|
|
2768
|
+
};
|
|
2769
|
+
}
|
|
2770
|
+
// Test notification permission (often different in incognito)
|
|
2771
|
+
const result = await navigator.permissions.query({ name: 'notifications' });
|
|
2772
|
+
// In some browsers, incognito mode always returns "denied" for notifications
|
|
2773
|
+
if (result.state === 'denied') {
|
|
2774
|
+
return {
|
|
2775
|
+
detected: true,
|
|
2776
|
+
confidence: 40,
|
|
2777
|
+
method: 'permissions_notifications_denied',
|
|
2778
|
+
details: { state: result.state }
|
|
2779
|
+
};
|
|
2780
|
+
}
|
|
2781
|
+
return {
|
|
2782
|
+
detected: false,
|
|
2783
|
+
confidence: 30,
|
|
2784
|
+
method: 'permissions_working',
|
|
2785
|
+
details: { state: result.state }
|
|
2786
|
+
};
|
|
2787
|
+
}
|
|
2788
|
+
catch (error) {
|
|
2789
|
+
return {
|
|
2790
|
+
detected: true,
|
|
2791
|
+
confidence: 50,
|
|
2792
|
+
method: 'permissions_error',
|
|
2793
|
+
details: { error: error instanceof Error ? error.message : 'Unknown' }
|
|
2794
|
+
};
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
/**
|
|
2798
|
+
* Test quota management API
|
|
2799
|
+
*/
|
|
2800
|
+
async function testQuotaManagement() {
|
|
2801
|
+
try {
|
|
2802
|
+
if (!navigator.storage || !navigator.storage.estimate) {
|
|
2803
|
+
return {
|
|
2804
|
+
detected: false,
|
|
2805
|
+
confidence: 10,
|
|
2806
|
+
method: 'quota_unsupported'
|
|
2807
|
+
};
|
|
2808
|
+
}
|
|
2809
|
+
const estimate = await navigator.storage.estimate();
|
|
2810
|
+
// Incognito mode often has very limited quota
|
|
2811
|
+
if (estimate.quota && estimate.quota < 10 * 1024 * 1024) { // Less than 10MB
|
|
2812
|
+
return {
|
|
2813
|
+
detected: true,
|
|
2814
|
+
confidence: 70,
|
|
2815
|
+
method: 'quota_limited',
|
|
2816
|
+
details: { quota: estimate.quota, usage: estimate.usage }
|
|
2817
|
+
};
|
|
2818
|
+
}
|
|
2819
|
+
return {
|
|
2820
|
+
detected: false,
|
|
2821
|
+
confidence: 40,
|
|
2822
|
+
method: 'quota_normal',
|
|
2823
|
+
details: { quota: estimate.quota, usage: estimate.usage }
|
|
2824
|
+
};
|
|
2825
|
+
}
|
|
2826
|
+
catch (error) {
|
|
2827
|
+
return {
|
|
2828
|
+
detected: true,
|
|
2829
|
+
confidence: 60,
|
|
2830
|
+
method: 'quota_error',
|
|
2831
|
+
details: { error: error instanceof Error ? error.message : 'Unknown' }
|
|
2832
|
+
};
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
/**
|
|
2836
|
+
* Test filesystem API availability
|
|
2837
|
+
*/
|
|
2838
|
+
async function testFilesystem() {
|
|
2839
|
+
try {
|
|
2840
|
+
const requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem;
|
|
2841
|
+
if (!requestFileSystem) {
|
|
2842
|
+
return {
|
|
2843
|
+
detected: false,
|
|
2844
|
+
confidence: 10,
|
|
2845
|
+
method: 'filesystem_unsupported'
|
|
2846
|
+
};
|
|
2847
|
+
}
|
|
2848
|
+
return new Promise((resolve) => {
|
|
2849
|
+
requestFileSystem(window.TEMPORARY || 0, 1024, () => {
|
|
2850
|
+
resolve({
|
|
2851
|
+
detected: false,
|
|
2852
|
+
confidence: 30,
|
|
2853
|
+
method: 'filesystem_working'
|
|
2854
|
+
});
|
|
2855
|
+
}, (error) => {
|
|
2856
|
+
resolve({
|
|
2857
|
+
detected: true,
|
|
2858
|
+
confidence: 60,
|
|
2859
|
+
method: 'filesystem_error',
|
|
2860
|
+
details: { error: error.name || 'Unknown' }
|
|
2861
|
+
});
|
|
2862
|
+
});
|
|
2863
|
+
// Timeout
|
|
2864
|
+
setTimeout(() => {
|
|
2865
|
+
resolve({
|
|
2866
|
+
detected: true,
|
|
2867
|
+
confidence: 50,
|
|
2868
|
+
method: 'filesystem_timeout'
|
|
2869
|
+
});
|
|
2870
|
+
}, 1000);
|
|
2871
|
+
});
|
|
2872
|
+
}
|
|
2873
|
+
catch (error) {
|
|
2874
|
+
return {
|
|
2875
|
+
detected: true,
|
|
2876
|
+
confidence: 70,
|
|
2877
|
+
method: 'filesystem_exception',
|
|
2878
|
+
details: { error: error instanceof Error ? error.message : 'Unknown' }
|
|
2879
|
+
};
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
/**
|
|
2883
|
+
* Test cookies enabled state
|
|
2884
|
+
*/
|
|
2885
|
+
async function testCookiesEnabled() {
|
|
2886
|
+
try {
|
|
2887
|
+
const cookiesEnabled = navigator.cookieEnabled;
|
|
2888
|
+
// Test by setting a cookie
|
|
2889
|
+
document.cookie = '__incognito_test=test; path=/';
|
|
2890
|
+
const cookieSet = document.cookie.includes('__incognito_test=test');
|
|
2891
|
+
// Clean up
|
|
2892
|
+
document.cookie = '__incognito_test=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
|
|
2893
|
+
if (!cookiesEnabled || !cookieSet) {
|
|
2894
|
+
return {
|
|
2895
|
+
detected: true,
|
|
2896
|
+
confidence: 50,
|
|
2897
|
+
method: 'cookies_disabled',
|
|
2898
|
+
details: { cookiesEnabled, cookieSet }
|
|
2899
|
+
};
|
|
2900
|
+
}
|
|
2901
|
+
return {
|
|
2902
|
+
detected: false,
|
|
2903
|
+
confidence: 20,
|
|
2904
|
+
method: 'cookies_working',
|
|
2905
|
+
details: { cookiesEnabled, cookieSet }
|
|
2906
|
+
};
|
|
2907
|
+
}
|
|
2908
|
+
catch (error) {
|
|
2909
|
+
return {
|
|
2910
|
+
detected: true,
|
|
2911
|
+
confidence: 60,
|
|
2912
|
+
method: 'cookies_error',
|
|
2913
|
+
details: { error: error instanceof Error ? error.message : 'Unknown' }
|
|
2914
|
+
};
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
/**
|
|
2918
|
+
* Test cache detection through timing
|
|
2919
|
+
*/
|
|
2920
|
+
async function testCacheDetection() {
|
|
2921
|
+
try {
|
|
2922
|
+
const image = new Image();
|
|
2923
|
+
const startTime = performance.now();
|
|
2924
|
+
return new Promise((resolve) => {
|
|
2925
|
+
const timeout = setTimeout(() => {
|
|
2926
|
+
resolve({
|
|
2927
|
+
detected: false,
|
|
2928
|
+
confidence: 10,
|
|
2929
|
+
method: 'cache_timeout'
|
|
2930
|
+
});
|
|
2931
|
+
}, 3000);
|
|
2932
|
+
image.onload = () => {
|
|
2933
|
+
clearTimeout(timeout);
|
|
2934
|
+
const loadTime = performance.now() - startTime;
|
|
2935
|
+
// Very fast load might indicate cache is disabled (incognito)
|
|
2936
|
+
if (loadTime < 5) {
|
|
2937
|
+
resolve({
|
|
2938
|
+
detected: true,
|
|
2939
|
+
confidence: 30,
|
|
2940
|
+
method: 'cache_too_fast',
|
|
2941
|
+
details: { loadTime }
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
2944
|
+
else {
|
|
2945
|
+
resolve({
|
|
2946
|
+
detected: false,
|
|
2947
|
+
confidence: 20,
|
|
2948
|
+
method: 'cache_normal',
|
|
2949
|
+
details: { loadTime }
|
|
2950
|
+
});
|
|
2951
|
+
}
|
|
2952
|
+
};
|
|
2953
|
+
image.onerror = () => {
|
|
2954
|
+
clearTimeout(timeout);
|
|
2955
|
+
resolve({
|
|
2956
|
+
detected: false,
|
|
2957
|
+
confidence: 10,
|
|
2958
|
+
method: 'cache_error'
|
|
2959
|
+
});
|
|
2960
|
+
};
|
|
2961
|
+
// Use a small, commonly cached image
|
|
2962
|
+
image.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
|
2963
|
+
});
|
|
2964
|
+
}
|
|
2965
|
+
catch (error) {
|
|
2966
|
+
return {
|
|
2967
|
+
detected: false,
|
|
2968
|
+
confidence: 10,
|
|
2969
|
+
method: 'cache_exception',
|
|
2970
|
+
details: { error: error instanceof Error ? error.message : 'Unknown' }
|
|
2971
|
+
};
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
/**
|
|
2975
|
+
* Detect browser-specific incognito patterns
|
|
2976
|
+
*/
|
|
2977
|
+
function detectBrowserSpecificPatterns() {
|
|
2978
|
+
const userAgent = navigator.userAgent.toLowerCase();
|
|
2979
|
+
if (userAgent.includes('chrome')) {
|
|
2980
|
+
return { browser: 'Chrome', supportsDetection: true };
|
|
2981
|
+
}
|
|
2982
|
+
else if (userAgent.includes('firefox')) {
|
|
2983
|
+
return { browser: 'Firefox', supportsDetection: true };
|
|
2984
|
+
}
|
|
2985
|
+
else if (userAgent.includes('safari')) {
|
|
2986
|
+
return { browser: 'Safari', supportsDetection: true };
|
|
2987
|
+
}
|
|
2988
|
+
else if (userAgent.includes('edge')) {
|
|
2989
|
+
return { browser: 'Edge', supportsDetection: true };
|
|
2990
|
+
}
|
|
2991
|
+
return { browser: 'Unknown', supportsDetection: false };
|
|
2992
|
+
}
|
|
2993
|
+
/**
|
|
2994
|
+
* Main incognito detection function
|
|
2995
|
+
*/
|
|
2996
|
+
async function detectIncognitoMode() {
|
|
2997
|
+
detectBrowserSpecificPatterns();
|
|
2998
|
+
// Run all detection methods in parallel
|
|
2999
|
+
const [localStorage, sessionStorage, indexedDB, openDatabase, permissions, quotaManagement, filesystem, cookiesEnabled, cacheDetection] = await Promise.all([
|
|
3000
|
+
testLocalStorage(),
|
|
3001
|
+
testSessionStorage(),
|
|
3002
|
+
testIndexedDB(),
|
|
3003
|
+
testOpenDatabase(),
|
|
3004
|
+
testPermissions(),
|
|
3005
|
+
testQuotaManagement(),
|
|
3006
|
+
testFilesystem(),
|
|
3007
|
+
testCookiesEnabled(),
|
|
3008
|
+
testCacheDetection()
|
|
3009
|
+
]);
|
|
3010
|
+
const detectionMethods = {
|
|
3011
|
+
localStorage,
|
|
3012
|
+
sessionStorage,
|
|
3013
|
+
indexedDB,
|
|
3014
|
+
openDatabase,
|
|
3015
|
+
permissions,
|
|
3016
|
+
quotaManagement,
|
|
3017
|
+
filesystem,
|
|
3018
|
+
cookiesEnabled,
|
|
3019
|
+
cacheDetection,
|
|
3020
|
+
canvasConsistency: { detected: false, confidence: 0, method: 'not_implemented' },
|
|
3021
|
+
webglConsistency: { detected: false, confidence: 0, method: 'not_implemented' },
|
|
3022
|
+
timingDifferences: { detected: false, confidence: 0, method: 'not_implemented' }
|
|
3023
|
+
};
|
|
3024
|
+
// Calculate overall confidence
|
|
3025
|
+
const detections = Object.values(detectionMethods).filter(d => d.detected);
|
|
3026
|
+
const totalConfidence = detections.reduce((sum, d) => sum + d.confidence, 0);
|
|
3027
|
+
const averageConfidence = detections.length > 0 ? totalConfidence / detections.length : 0;
|
|
3028
|
+
// Determine if incognito mode is detected
|
|
3029
|
+
const isIncognito = detections.length >= 2 || // Multiple methods detected
|
|
3030
|
+
detections.some(d => d.confidence >= 80); // High confidence detection
|
|
3031
|
+
const confidence = Math.min(averageConfidence, 95); // Cap at 95%
|
|
3032
|
+
return {
|
|
3033
|
+
isIncognito,
|
|
3034
|
+
confidence: Math.round(confidence),
|
|
3035
|
+
detectionMethods: Object.keys(detectionMethods).filter(key => detectionMethods[key]?.detected),
|
|
3036
|
+
details: {
|
|
3037
|
+
localStorage: localStorage?.detected ?? false,
|
|
3038
|
+
sessionStorage: sessionStorage?.detected ?? false,
|
|
3039
|
+
indexedDB: indexedDB?.detected ?? false,
|
|
3040
|
+
cookiesEnabled: cookiesEnabled?.detected ?? false,
|
|
3041
|
+
quota: quotaManagement?.detected ? 0 : null,
|
|
3042
|
+
permissions: permissions?.details || {}
|
|
3043
|
+
}
|
|
3044
|
+
};
|
|
3045
|
+
}
|
|
3046
|
+
/**
|
|
3047
|
+
* Quick incognito detection (less comprehensive but faster)
|
|
3048
|
+
*/
|
|
3049
|
+
async function quickIncognitoDetection() {
|
|
3050
|
+
try {
|
|
3051
|
+
// Test the most reliable indicators quickly
|
|
3052
|
+
const [localStorage, indexedDB] = await Promise.all([
|
|
3053
|
+
testLocalStorage(),
|
|
3054
|
+
testIndexedDB()
|
|
3055
|
+
]);
|
|
3056
|
+
return localStorage.detected || indexedDB.detected;
|
|
3057
|
+
}
|
|
3058
|
+
catch {
|
|
3059
|
+
return false;
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
/**
|
|
3064
|
+
* Enhanced User Agent Parser
|
|
3065
|
+
* Based on FingerprintJS browser detection with advanced engine and version parsing
|
|
3066
|
+
*/
|
|
3067
|
+
/**
|
|
3068
|
+
* Browser engine detection (based on FingerprintJS)
|
|
3069
|
+
*/
|
|
3070
|
+
function isChromium() {
|
|
3071
|
+
try {
|
|
3072
|
+
return typeof window.chrome === 'object' &&
|
|
3073
|
+
window.chrome !== null &&
|
|
3074
|
+
typeof window.chrome.runtime === 'object';
|
|
3075
|
+
}
|
|
3076
|
+
catch {
|
|
3077
|
+
return false;
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
function isWebKit() {
|
|
3081
|
+
return navigator.userAgent.includes('WebKit') &&
|
|
3082
|
+
!navigator.userAgent.includes('Chrome');
|
|
3083
|
+
}
|
|
3084
|
+
function isGecko() {
|
|
3085
|
+
return navigator.userAgent.includes('Gecko') &&
|
|
3086
|
+
!navigator.userAgent.includes('Chrome');
|
|
3087
|
+
}
|
|
3088
|
+
function isSamsungInternet() {
|
|
3089
|
+
return navigator.userAgent.includes('SamsungBrowser') ||
|
|
3090
|
+
navigator.userAgent.includes('Samsung Internet');
|
|
3091
|
+
}
|
|
3092
|
+
function isIPad() {
|
|
3093
|
+
// iPad detection (iOS 13+ shows as desktop Safari)
|
|
3094
|
+
return navigator.userAgent.includes('iPad') ||
|
|
3095
|
+
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
|
3096
|
+
}
|
|
3097
|
+
function isAndroid() {
|
|
3098
|
+
return navigator.userAgent.includes('Android');
|
|
3099
|
+
}
|
|
3100
|
+
/**
|
|
3101
|
+
* Extract browser version from user agent
|
|
3102
|
+
*/
|
|
3103
|
+
function extractVersion(userAgent, pattern) {
|
|
3104
|
+
const match = userAgent.match(pattern);
|
|
3105
|
+
return match ? (match[1] || 'Unknown') : 'Unknown';
|
|
3106
|
+
}
|
|
3107
|
+
/**
|
|
3108
|
+
* Detect browser name and version
|
|
3109
|
+
*/
|
|
3110
|
+
function detectBrowser(userAgent) {
|
|
3111
|
+
const ua = userAgent.toLowerCase();
|
|
3112
|
+
// Edge (Chromium-based)
|
|
3113
|
+
if (ua.includes('edg/')) {
|
|
3114
|
+
const version = extractVersion(userAgent, /edg\/([0-9.]+)/i);
|
|
3115
|
+
return {
|
|
3116
|
+
name: 'Edge',
|
|
3117
|
+
version,
|
|
3118
|
+
major: version.split('.')[0] || 'Unknown',
|
|
3119
|
+
engine: 'chromium'
|
|
3120
|
+
};
|
|
3121
|
+
}
|
|
3122
|
+
// Edge Legacy
|
|
3123
|
+
if (ua.includes('edge/')) {
|
|
3124
|
+
const version = extractVersion(userAgent, /edge\/([0-9.]+)/i);
|
|
3125
|
+
return {
|
|
3126
|
+
name: 'Edge Legacy',
|
|
3127
|
+
version,
|
|
3128
|
+
major: version.split('.')[0] || 'Unknown',
|
|
3129
|
+
engine: 'edgehtml'
|
|
3130
|
+
};
|
|
3131
|
+
}
|
|
3132
|
+
// Brave Browser (check before Chrome)
|
|
3133
|
+
if (ua.includes('brave/') || navigator.brave) {
|
|
3134
|
+
const version = extractVersion(userAgent, /chrome\/([0-9.]+)/i);
|
|
3135
|
+
return {
|
|
3136
|
+
name: 'Brave',
|
|
3137
|
+
version,
|
|
3138
|
+
major: version.split('.')[0] || 'Unknown',
|
|
3139
|
+
engine: 'chromium'
|
|
3140
|
+
};
|
|
3141
|
+
}
|
|
3142
|
+
// Samsung Internet
|
|
3143
|
+
if (isSamsungInternet()) {
|
|
3144
|
+
const version = extractVersion(userAgent, /samsungbrowser\/([0-9.]+)/i);
|
|
3145
|
+
return {
|
|
3146
|
+
name: 'Samsung Internet',
|
|
3147
|
+
version,
|
|
3148
|
+
major: version.split('.')[0] || 'Unknown',
|
|
3149
|
+
engine: 'chromium'
|
|
3150
|
+
};
|
|
3151
|
+
}
|
|
3152
|
+
// Chrome (check after other Chromium browsers)
|
|
3153
|
+
if (ua.includes('chrome/') && !ua.includes('edg')) {
|
|
3154
|
+
const version = extractVersion(userAgent, /chrome\/([0-9.]+)/i);
|
|
3155
|
+
return {
|
|
3156
|
+
name: 'Chrome',
|
|
3157
|
+
version,
|
|
3158
|
+
major: version.split('.')[0] || 'Unknown',
|
|
3159
|
+
engine: 'chromium'
|
|
3160
|
+
};
|
|
3161
|
+
}
|
|
3162
|
+
// Firefox
|
|
3163
|
+
if (ua.includes('firefox/')) {
|
|
3164
|
+
const version = extractVersion(userAgent, /firefox\/([0-9.]+)/i);
|
|
3165
|
+
return {
|
|
3166
|
+
name: 'Firefox',
|
|
3167
|
+
version,
|
|
3168
|
+
major: version.split('.')[0] || 'Unknown',
|
|
3169
|
+
engine: 'gecko'
|
|
3170
|
+
};
|
|
3171
|
+
}
|
|
3172
|
+
// Safari (check after Chrome to avoid false positives)
|
|
3173
|
+
if (ua.includes('safari/') && !ua.includes('chrome')) {
|
|
3174
|
+
const version = extractVersion(userAgent, /version\/([0-9.]+)/i);
|
|
3175
|
+
return {
|
|
3176
|
+
name: 'Safari',
|
|
3177
|
+
version,
|
|
3178
|
+
major: version.split('.')[0] || 'Unknown',
|
|
3179
|
+
engine: 'webkit'
|
|
3180
|
+
};
|
|
3181
|
+
}
|
|
3182
|
+
// Opera (modern versions)
|
|
3183
|
+
if (ua.includes('opr/') || ua.includes('opera/')) {
|
|
3184
|
+
const version = extractVersion(userAgent, /(?:opr|opera)\/([0-9.]+)/i);
|
|
3185
|
+
return {
|
|
3186
|
+
name: 'Opera',
|
|
3187
|
+
version,
|
|
3188
|
+
major: version.split('.')[0] || 'Unknown',
|
|
3189
|
+
engine: isChromium() ? 'chromium' : 'unknown'
|
|
3190
|
+
};
|
|
3191
|
+
}
|
|
3192
|
+
// Vivaldi
|
|
3193
|
+
if (ua.includes('vivaldi/')) {
|
|
3194
|
+
const version = extractVersion(userAgent, /vivaldi\/([0-9.]+)/i);
|
|
3195
|
+
return {
|
|
3196
|
+
name: 'Vivaldi',
|
|
3197
|
+
version,
|
|
3198
|
+
major: version.split('.')[0] || 'Unknown',
|
|
3199
|
+
engine: 'chromium'
|
|
3200
|
+
};
|
|
3201
|
+
}
|
|
3202
|
+
// Yandex Browser
|
|
3203
|
+
if (ua.includes('yabrowser/')) {
|
|
3204
|
+
const version = extractVersion(userAgent, /yabrowser\/([0-9.]+)/i);
|
|
3205
|
+
return {
|
|
3206
|
+
name: 'Yandex Browser',
|
|
3207
|
+
version,
|
|
3208
|
+
major: version.split('.')[0] || 'Unknown',
|
|
3209
|
+
engine: 'chromium'
|
|
3210
|
+
};
|
|
3211
|
+
}
|
|
3212
|
+
// Internet Explorer
|
|
3213
|
+
if (ua.includes('msie') || ua.includes('trident/')) {
|
|
3214
|
+
const version = extractVersion(userAgent, /(?:msie |rv:)([0-9.]+)/i);
|
|
3215
|
+
return {
|
|
3216
|
+
name: 'Internet Explorer',
|
|
3217
|
+
version,
|
|
3218
|
+
major: version.split('.')[0] || 'Unknown',
|
|
3219
|
+
engine: 'trident'
|
|
3220
|
+
};
|
|
3221
|
+
}
|
|
3222
|
+
// UC Browser
|
|
3223
|
+
if (ua.includes('ucbrowser/')) {
|
|
3224
|
+
const version = extractVersion(userAgent, /ucbrowser\/([0-9.]+)/i);
|
|
3225
|
+
return {
|
|
3226
|
+
name: 'UC Browser',
|
|
3227
|
+
version,
|
|
3228
|
+
major: version.split('.')[0] || 'Unknown',
|
|
3229
|
+
engine: 'chromium'
|
|
3230
|
+
};
|
|
3231
|
+
}
|
|
3232
|
+
// QQ Browser
|
|
3233
|
+
if (ua.includes('qqbrowser/')) {
|
|
3234
|
+
const version = extractVersion(userAgent, /qqbrowser\/([0-9.]+)/i);
|
|
3235
|
+
return {
|
|
3236
|
+
name: 'QQ Browser',
|
|
3237
|
+
version,
|
|
3238
|
+
major: version.split('.')[0] || 'Unknown',
|
|
3239
|
+
engine: 'chromium'
|
|
3240
|
+
};
|
|
3241
|
+
}
|
|
3242
|
+
// Unknown browser
|
|
3243
|
+
return {
|
|
3244
|
+
name: 'Unknown',
|
|
3245
|
+
version: 'Unknown',
|
|
3246
|
+
major: 'Unknown',
|
|
3247
|
+
engine: 'unknown'
|
|
3248
|
+
};
|
|
3249
|
+
}
|
|
3250
|
+
/**
|
|
3251
|
+
* Detect operating system
|
|
3252
|
+
*/
|
|
3253
|
+
function detectOS(userAgent) {
|
|
3254
|
+
const ua = userAgent.toLowerCase();
|
|
3255
|
+
// Windows
|
|
3256
|
+
if (ua.includes('windows nt')) {
|
|
3257
|
+
const version = extractVersion(userAgent, /windows nt ([0-9.]+)/i);
|
|
3258
|
+
let osName = 'Windows';
|
|
3259
|
+
// Map NT versions to friendly names
|
|
3260
|
+
switch (version) {
|
|
3261
|
+
case '10.0':
|
|
3262
|
+
osName = 'Windows 10/11';
|
|
3263
|
+
break;
|
|
3264
|
+
case '6.3':
|
|
3265
|
+
osName = 'Windows 8.1';
|
|
3266
|
+
break;
|
|
3267
|
+
case '6.2':
|
|
3268
|
+
osName = 'Windows 8';
|
|
3269
|
+
break;
|
|
3270
|
+
case '6.1':
|
|
3271
|
+
osName = 'Windows 7';
|
|
3272
|
+
break;
|
|
3273
|
+
case '6.0':
|
|
3274
|
+
osName = 'Windows Vista';
|
|
3275
|
+
break;
|
|
3276
|
+
case '5.1':
|
|
3277
|
+
osName = 'Windows XP';
|
|
3278
|
+
break;
|
|
3279
|
+
default: osName = 'Windows';
|
|
3280
|
+
}
|
|
3281
|
+
return {
|
|
3282
|
+
name: osName,
|
|
3283
|
+
version,
|
|
3284
|
+
family: 'windows'
|
|
3285
|
+
};
|
|
3286
|
+
}
|
|
3287
|
+
// macOS / Mac OS X
|
|
3288
|
+
if (ua.includes('mac os x') || ua.includes('macos')) {
|
|
3289
|
+
const version = extractVersion(userAgent, /mac os x ([0-9._]+)/i);
|
|
3290
|
+
return {
|
|
3291
|
+
name: 'macOS',
|
|
3292
|
+
version: version.replace(/_/g, '.'),
|
|
3293
|
+
family: 'macos'
|
|
3294
|
+
};
|
|
3295
|
+
}
|
|
3296
|
+
// iOS
|
|
3297
|
+
if (ua.includes('iphone') || ua.includes('ipad') || ua.includes('ipod') || isIPad()) {
|
|
3298
|
+
const version = extractVersion(userAgent, /os ([0-9._]+)/i);
|
|
3299
|
+
return {
|
|
3300
|
+
name: 'iOS',
|
|
3301
|
+
version: version.replace(/_/g, '.'),
|
|
3302
|
+
family: 'ios'
|
|
3303
|
+
};
|
|
3304
|
+
}
|
|
3305
|
+
// Android
|
|
3306
|
+
if (isAndroid()) {
|
|
3307
|
+
const version = extractVersion(userAgent, /android ([0-9.]+)/i);
|
|
3308
|
+
return {
|
|
3309
|
+
name: 'Android',
|
|
3310
|
+
version,
|
|
3311
|
+
family: 'android'
|
|
3312
|
+
};
|
|
3313
|
+
}
|
|
3314
|
+
// Linux
|
|
3315
|
+
if (ua.includes('linux')) {
|
|
3316
|
+
return {
|
|
3317
|
+
name: 'Linux',
|
|
3318
|
+
version: 'Unknown',
|
|
3319
|
+
family: 'linux'
|
|
3320
|
+
};
|
|
3321
|
+
}
|
|
3322
|
+
// Chrome OS
|
|
3323
|
+
if (ua.includes('cros')) {
|
|
3324
|
+
const version = extractVersion(userAgent, /cros [a-z0-9_]+ ([0-9.]+)/i);
|
|
3325
|
+
return {
|
|
3326
|
+
name: 'Chrome OS',
|
|
3327
|
+
version,
|
|
3328
|
+
family: 'linux'
|
|
3329
|
+
};
|
|
3330
|
+
}
|
|
3331
|
+
// FreeBSD
|
|
3332
|
+
if (ua.includes('freebsd')) {
|
|
3333
|
+
return {
|
|
3334
|
+
name: 'FreeBSD',
|
|
3335
|
+
version: 'Unknown',
|
|
3336
|
+
family: 'unix'
|
|
3337
|
+
};
|
|
3338
|
+
}
|
|
3339
|
+
// Unknown OS
|
|
3340
|
+
return {
|
|
3341
|
+
name: 'Unknown',
|
|
3342
|
+
version: 'Unknown',
|
|
3343
|
+
family: 'unknown'
|
|
3344
|
+
};
|
|
3345
|
+
}
|
|
3346
|
+
/**
|
|
3347
|
+
* Detect device type
|
|
3348
|
+
*/
|
|
3349
|
+
function detectDevice(userAgent) {
|
|
3350
|
+
const ua = userAgent.toLowerCase();
|
|
3351
|
+
// Tablet detection
|
|
3352
|
+
if (ua.includes('tablet') || isIPad()) {
|
|
3353
|
+
if (isIPad()) {
|
|
3354
|
+
return { type: 'tablet', vendor: 'Apple', model: 'iPad' };
|
|
3355
|
+
}
|
|
3356
|
+
if (ua.includes('kindle')) {
|
|
3357
|
+
return { type: 'tablet', vendor: 'Amazon', model: 'Kindle' };
|
|
3358
|
+
}
|
|
3359
|
+
return { type: 'tablet' };
|
|
3360
|
+
}
|
|
3361
|
+
// Mobile detection
|
|
3362
|
+
if (ua.includes('mobile') ||
|
|
3363
|
+
ua.includes('iphone') ||
|
|
3364
|
+
ua.includes('ipod') ||
|
|
3365
|
+
isAndroid()) {
|
|
3366
|
+
if (ua.includes('iphone')) {
|
|
3367
|
+
return { type: 'mobile', vendor: 'Apple', model: 'iPhone' };
|
|
3368
|
+
}
|
|
3369
|
+
if (ua.includes('ipod')) {
|
|
3370
|
+
return { type: 'mobile', vendor: 'Apple', model: 'iPod' };
|
|
3371
|
+
}
|
|
3372
|
+
if (isAndroid() && ua.includes('mobile')) {
|
|
3373
|
+
return { type: 'mobile', vendor: 'Android' };
|
|
3374
|
+
}
|
|
3375
|
+
return { type: 'mobile' };
|
|
3376
|
+
}
|
|
3377
|
+
// Desktop (default)
|
|
3378
|
+
return { type: 'desktop' };
|
|
3379
|
+
}
|
|
3380
|
+
/**
|
|
3381
|
+
* Check for suspicious patterns in user agent
|
|
3382
|
+
*/
|
|
3383
|
+
function analyzeSuspiciousPatterns(userAgent) {
|
|
3384
|
+
const patterns = [];
|
|
3385
|
+
let category = 'legitimate';
|
|
3386
|
+
// Bot patterns
|
|
3387
|
+
const botKeywords = [
|
|
3388
|
+
'bot', 'crawler', 'spider', 'scraper', 'automation',
|
|
3389
|
+
'headless', 'phantom', 'selenium', 'webdriver',
|
|
3390
|
+
'google', 'bing', 'yahoo', 'baidu', 'yandex',
|
|
3391
|
+
'facebook', 'twitter', 'linkedin', 'whatsapp'
|
|
3392
|
+
];
|
|
3393
|
+
for (const keyword of botKeywords) {
|
|
3394
|
+
if (userAgent.toLowerCase().includes(keyword)) {
|
|
3395
|
+
patterns.push(keyword);
|
|
3396
|
+
if (['bot', 'crawler', 'spider', 'scraper'].some(k => keyword.includes(k))) {
|
|
3397
|
+
category = 'crawler';
|
|
3398
|
+
}
|
|
3399
|
+
else if (['selenium', 'webdriver', 'automation'].some(k => keyword.includes(k))) {
|
|
3400
|
+
category = 'automation';
|
|
3401
|
+
}
|
|
3402
|
+
else if (['headless', 'phantom'].some(k => keyword.includes(k))) {
|
|
3403
|
+
category = 'headless';
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
// Suspicious patterns
|
|
3408
|
+
if (userAgent.length < 10) {
|
|
3409
|
+
patterns.push('too_short');
|
|
3410
|
+
}
|
|
3411
|
+
if (!userAgent || userAgent === 'Mozilla/5.0') {
|
|
3412
|
+
patterns.push('generic_mozilla');
|
|
3413
|
+
}
|
|
3414
|
+
const isBot = patterns.length > 0;
|
|
3415
|
+
return { isBot, patterns, category };
|
|
3416
|
+
}
|
|
3417
|
+
/**
|
|
3418
|
+
* Enhanced user agent analysis
|
|
3419
|
+
*/
|
|
3420
|
+
function analyzeUserAgent(userAgent) {
|
|
3421
|
+
const ua = userAgent || navigator.userAgent || '';
|
|
3422
|
+
const browser = detectBrowser(ua);
|
|
3423
|
+
const os = detectOS(ua);
|
|
3424
|
+
const device = detectDevice(ua);
|
|
3425
|
+
const suspicious = analyzeSuspiciousPatterns(ua);
|
|
3426
|
+
return {
|
|
3427
|
+
browser: {
|
|
3428
|
+
name: browser.name,
|
|
3429
|
+
version: browser.version,
|
|
3430
|
+
major: browser.major,
|
|
3431
|
+
engine: browser.engine
|
|
3432
|
+
},
|
|
3433
|
+
os: {
|
|
3434
|
+
name: os.name,
|
|
3435
|
+
version: os.version,
|
|
3436
|
+
family: os.family
|
|
3437
|
+
},
|
|
3438
|
+
device: {
|
|
3439
|
+
type: device.type,
|
|
3440
|
+
vendor: device.vendor,
|
|
3441
|
+
model: device.model
|
|
3442
|
+
},
|
|
3443
|
+
suspicious: {
|
|
3444
|
+
isBot: suspicious.isBot,
|
|
3445
|
+
patterns: suspicious.patterns,
|
|
3446
|
+
category: suspicious.category
|
|
3447
|
+
},
|
|
3448
|
+
isMobile: device.type === 'mobile',
|
|
3449
|
+
isTablet: device.type === 'tablet',
|
|
3450
|
+
isDesktop: device.type === 'desktop',
|
|
3451
|
+
isBot: suspicious.isBot,
|
|
3452
|
+
raw: ua
|
|
3453
|
+
};
|
|
3454
|
+
}
|
|
3455
|
+
/**
|
|
3456
|
+
* Get browser engine type
|
|
3457
|
+
*/
|
|
3458
|
+
function getBrowserEngine() {
|
|
3459
|
+
if (isChromium())
|
|
3460
|
+
return 'chromium';
|
|
3461
|
+
if (isWebKit())
|
|
3462
|
+
return 'webkit';
|
|
3463
|
+
if (isGecko())
|
|
3464
|
+
return 'gecko';
|
|
3465
|
+
const ua = navigator.userAgent.toLowerCase();
|
|
3466
|
+
if (ua.includes('trident'))
|
|
3467
|
+
return 'trident';
|
|
3468
|
+
return 'unknown';
|
|
3469
|
+
}
|
|
3470
|
+
/**
|
|
3471
|
+
* Check if running on mobile device
|
|
3472
|
+
*/
|
|
3473
|
+
function isMobile() {
|
|
3474
|
+
return detectDevice(navigator.userAgent).type === 'mobile';
|
|
3475
|
+
}
|
|
3476
|
+
/**
|
|
3477
|
+
* Check if running on tablet
|
|
3478
|
+
*/
|
|
3479
|
+
function isTablet() {
|
|
3480
|
+
return detectDevice(navigator.userAgent).type === 'tablet';
|
|
3481
|
+
}
|
|
3482
|
+
/**
|
|
3483
|
+
* Check if running on desktop
|
|
3484
|
+
*/
|
|
3485
|
+
function isDesktop() {
|
|
3486
|
+
return detectDevice(navigator.userAgent).type === 'desktop';
|
|
3487
|
+
}
|
|
3488
|
+
|
|
3489
|
+
exports.RabbitTrackerSDK = RabbitTrackerSDK;
|
|
3490
|
+
exports.analyzeUserAgent = analyzeUserAgent;
|
|
3491
|
+
exports.clearFingerprintCache = clearFingerprintCache;
|
|
3492
|
+
exports.collectFingerprint = collectFingerprint;
|
|
3493
|
+
exports.default = RabbitTrackerSDK;
|
|
3494
|
+
exports.detectBot = detectBot;
|
|
3495
|
+
exports.detectIncognitoMode = detectIncognitoMode;
|
|
3496
|
+
exports.generateVisitorId = generateVisitorId;
|
|
3497
|
+
exports.generateVisitorIdFromFingerprint = generateVisitorIdFromFingerprint;
|
|
3498
|
+
exports.getAvailableComponents = getAvailableComponents;
|
|
3499
|
+
exports.getBrowserEngine = getBrowserEngine;
|
|
3500
|
+
exports.getCompleteFingerprint = getCompleteFingerprint;
|
|
3501
|
+
exports.getLightweightFingerprint = getLightweightFingerprint;
|
|
3502
|
+
exports.hash32 = hash32;
|
|
3503
|
+
exports.hashFingerprint = hashFingerprint;
|
|
3504
|
+
exports.isAndroid = isAndroid;
|
|
3505
|
+
exports.isChromium = isChromium;
|
|
3506
|
+
exports.isDesktop = isDesktop;
|
|
3507
|
+
exports.isFingerprintingAvailable = isFingerprintingAvailable;
|
|
3508
|
+
exports.isGecko = isGecko;
|
|
3509
|
+
exports.isIPad = isIPad;
|
|
3510
|
+
exports.isMobile = isMobile;
|
|
3511
|
+
exports.isSamsungInternet = isSamsungInternet;
|
|
3512
|
+
exports.isTablet = isTablet;
|
|
3513
|
+
exports.isWebKit = isWebKit;
|
|
3514
|
+
exports.quickBotDetection = quickBotDetection;
|
|
3515
|
+
exports.quickIncognitoDetection = quickIncognitoDetection;
|
|
3516
|
+
exports.x64hash128 = x64hash128;
|
|
3517
|
+
//# sourceMappingURL=index.js.map
|