@zaplier/sdk 1.0.8 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +330 -142
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.esm.js +330 -142
- package/dist/index.esm.js.map +1 -1
- package/dist/sdk.js +330 -142
- package/dist/sdk.js.map +1 -1
- package/dist/sdk.min.js +1 -1
- package/dist/src/modules/fingerprint/browser.d.ts.map +1 -1
- package/dist/src/modules/fingerprint/canvas.d.ts.map +1 -1
- package/dist/src/modules/fingerprint/device-signals.d.ts.map +1 -1
- package/dist/src/modules/fingerprint/hashing.d.ts.map +1 -1
- package/dist/src/modules/fingerprint/webgl.d.ts.map +1 -1
- package/dist/src/modules/fingerprint.d.ts.map +1 -1
- package/dist/src/sdk.d.ts +5 -0
- package/dist/src/sdk.d.ts.map +1 -1
- package/dist/src/utils/webgl-cache.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -260,8 +260,22 @@ function canonicalizeStable(input) {
|
|
|
260
260
|
return out;
|
|
261
261
|
}
|
|
262
262
|
if (typeof input === "number") {
|
|
263
|
-
//
|
|
264
|
-
|
|
263
|
+
// Enhanced floating point normalization for maximum stability
|
|
264
|
+
if (!Number.isFinite(input))
|
|
265
|
+
return 0;
|
|
266
|
+
// Handle very small numbers (round to 0)
|
|
267
|
+
if (Math.abs(input) < 0.0001)
|
|
268
|
+
return 0;
|
|
269
|
+
// Handle very large numbers (clamp to reasonable range)
|
|
270
|
+
if (Math.abs(input) > 1000000) {
|
|
271
|
+
return input > 0 ? 1000000 : -1e6;
|
|
272
|
+
}
|
|
273
|
+
// For screen ratios and similar values, use more precision
|
|
274
|
+
if (input >= 0.1 && input <= 10) {
|
|
275
|
+
return Number(input.toFixed(3));
|
|
276
|
+
}
|
|
277
|
+
// For other numbers, use 2 decimal places
|
|
278
|
+
return Number(input.toFixed(2));
|
|
265
279
|
}
|
|
266
280
|
if (typeof input === "string") {
|
|
267
281
|
return input.trim().toLowerCase();
|
|
@@ -1236,15 +1250,18 @@ function getTimezoneInfo() {
|
|
|
1236
1250
|
// Fallback
|
|
1237
1251
|
}
|
|
1238
1252
|
}
|
|
1239
|
-
// Fallback: use timezone offset
|
|
1253
|
+
// Fallback: use timezone offset (STABLE - use standard time offset to avoid DST changes)
|
|
1240
1254
|
if (!timezone) {
|
|
1241
|
-
|
|
1255
|
+
// Use January date to get standard time offset (avoiding DST variations)
|
|
1256
|
+
const januaryDate = new Date(2024, 0, 1); // January 1st
|
|
1257
|
+
const offset = januaryDate.getTimezoneOffset();
|
|
1242
1258
|
const sign = offset > 0 ? '-' : '+';
|
|
1243
1259
|
const hours = Math.floor(Math.abs(offset) / 60);
|
|
1244
1260
|
const minutes = Math.abs(offset) % 60;
|
|
1245
1261
|
timezone = `UTC${sign}${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
|
|
1246
1262
|
}
|
|
1247
|
-
|
|
1263
|
+
// Use January date for stable timezone offset (avoiding DST fluctuations)
|
|
1264
|
+
const timezoneOffset = new Date(2024, 0, 1).getTimezoneOffset();
|
|
1248
1265
|
return { timezone, timezoneOffset };
|
|
1249
1266
|
}
|
|
1250
1267
|
catch {
|
|
@@ -1592,25 +1609,40 @@ function analyzeSubPixels(ctx, canvas) {
|
|
|
1592
1609
|
// Get pixel data for analysis
|
|
1593
1610
|
const imageData = ctx.getImageData(10, 10, 30, 40);
|
|
1594
1611
|
const pixels = imageData.data;
|
|
1595
|
-
// Calculate sub-pixel characteristics
|
|
1612
|
+
// Calculate sub-pixel characteristics with enhanced stability
|
|
1596
1613
|
let redSum = 0, greenSum = 0, blueSum = 0;
|
|
1597
1614
|
let edgeVariance = 0;
|
|
1615
|
+
let validPixels = 0;
|
|
1598
1616
|
for (let i = 0; i < pixels.length; i += 4) {
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1617
|
+
const r = pixels[i] || 0;
|
|
1618
|
+
const g = pixels[i + 1] || 0;
|
|
1619
|
+
const b = pixels[i + 2] || 0;
|
|
1620
|
+
const a = pixels[i + 3] || 0;
|
|
1621
|
+
// Only process non-transparent pixels for stability
|
|
1622
|
+
if (a > 25) { // Higher threshold than before
|
|
1623
|
+
redSum += r;
|
|
1624
|
+
greenSum += g;
|
|
1625
|
+
blueSum += b;
|
|
1626
|
+
validPixels++;
|
|
1627
|
+
// Calculate edge variance with larger step for stability
|
|
1628
|
+
if (i >= 16) { // Skip more pixels for stability
|
|
1629
|
+
const prevR = pixels[i - 16] || 0;
|
|
1630
|
+
const diff = Math.abs(r - prevR);
|
|
1631
|
+
// Only count significant differences to reduce noise
|
|
1632
|
+
if (diff >= 8) { // Higher threshold
|
|
1633
|
+
edgeVariance += diff;
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
if (validPixels === 0) {
|
|
1639
|
+
return "no-valid-pixels";
|
|
1640
|
+
}
|
|
1641
|
+
// Round to nearest 10 for maximum stability
|
|
1642
|
+
const avgRed = Math.round((redSum / validPixels) / 10) * 10;
|
|
1643
|
+
const avgGreen = Math.round((greenSum / validPixels) / 10) * 10;
|
|
1644
|
+
const avgBlue = Math.round((blueSum / validPixels) / 10) * 10;
|
|
1645
|
+
const avgVariance = Math.round((edgeVariance / validPixels) / 10) * 10;
|
|
1614
1646
|
return `${avgRed}-${avgGreen}-${avgBlue}-${avgVariance}`;
|
|
1615
1647
|
}
|
|
1616
1648
|
catch (error) {
|
|
@@ -2212,7 +2244,8 @@ const MAX_CONTEXTS = 8;
|
|
|
2212
2244
|
*/
|
|
2213
2245
|
function getCachedWebGLContext() {
|
|
2214
2246
|
try {
|
|
2215
|
-
|
|
2247
|
+
// Use performance.now() for more stable timing measurements
|
|
2248
|
+
const now = performance.now();
|
|
2216
2249
|
// Check if cache is valid and not expired
|
|
2217
2250
|
if (webglCache && webglCache.isValid) {
|
|
2218
2251
|
if (now - webglCache.timestamp < CACHE_TIMEOUT) {
|
|
@@ -2673,12 +2706,14 @@ function render3DScene(gl) {
|
|
|
2673
2706
|
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
|
|
2674
2707
|
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
|
|
2675
2708
|
if (!vertexShader || !fragmentShader) {
|
|
2676
|
-
|
|
2709
|
+
// Fallback to basic parameters hash if shader compilation fails
|
|
2710
|
+
return generateFallbackWebGLHash(gl);
|
|
2677
2711
|
}
|
|
2678
2712
|
// Create program
|
|
2679
2713
|
const program = createProgram(gl, vertexShader, fragmentShader);
|
|
2680
2714
|
if (!program) {
|
|
2681
|
-
|
|
2715
|
+
// Fallback to basic parameters hash if program creation fails
|
|
2716
|
+
return generateFallbackWebGLHash(gl);
|
|
2682
2717
|
}
|
|
2683
2718
|
gl.useProgram(program);
|
|
2684
2719
|
// Create complex geometry that differentiates GPU rendering
|
|
@@ -2735,7 +2770,35 @@ function render3DScene(gl) {
|
|
|
2735
2770
|
return hash.toString(16);
|
|
2736
2771
|
}
|
|
2737
2772
|
catch (error) {
|
|
2738
|
-
|
|
2773
|
+
// Fallback to basic parameters hash if rendering fails
|
|
2774
|
+
return generateFallbackWebGLHash(gl);
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
/**
|
|
2778
|
+
* Generate fallback WebGL hash when rendering fails
|
|
2779
|
+
* Uses basic WebGL parameters to ensure consistent fingerprinting
|
|
2780
|
+
*/
|
|
2781
|
+
function generateFallbackWebGLHash(gl) {
|
|
2782
|
+
try {
|
|
2783
|
+
// Collect basic but stable WebGL parameters
|
|
2784
|
+
const vendor = gl.getParameter(gl.VENDOR) || 'unknown';
|
|
2785
|
+
const renderer = gl.getParameter(gl.RENDERER) || 'unknown';
|
|
2786
|
+
const version = gl.getParameter(gl.VERSION) || 'unknown';
|
|
2787
|
+
const maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE) || 0;
|
|
2788
|
+
const maxVertexAttribs = gl.getParameter(gl.MAX_VERTEX_ATTRIBS) || 0;
|
|
2789
|
+
const maxViewportDims = gl.getParameter(gl.MAX_VIEWPORT_DIMS) || [0, 0];
|
|
2790
|
+
// Combine parameters into string
|
|
2791
|
+
const combined = `${vendor}|${renderer}|${version}|${maxTextureSize}|${maxVertexAttribs}|${maxViewportDims}`;
|
|
2792
|
+
// Generate simple hash
|
|
2793
|
+
let hash = 0;
|
|
2794
|
+
for (let i = 0; i < combined.length; i++) {
|
|
2795
|
+
const char = combined.charCodeAt(i);
|
|
2796
|
+
hash = ((hash << 5) - hash + char) & 0xffffffff;
|
|
2797
|
+
}
|
|
2798
|
+
return `fallback_${Math.abs(hash).toString(16)}`;
|
|
2799
|
+
}
|
|
2800
|
+
catch {
|
|
2801
|
+
return 'webgl_unavailable';
|
|
2739
2802
|
}
|
|
2740
2803
|
}
|
|
2741
2804
|
/**
|
|
@@ -5069,91 +5132,67 @@ async function collectFingerprint(options = {}) {
|
|
|
5069
5132
|
gdprMode: opts.gdprMode,
|
|
5070
5133
|
components: [],
|
|
5071
5134
|
};
|
|
5072
|
-
// Collect
|
|
5135
|
+
// OPTIMIZATION: Collect independent fingerprint components in parallel
|
|
5136
|
+
// This reduces total collection time from 3-5s to 500ms-1s
|
|
5137
|
+
const parallelTasks = [];
|
|
5138
|
+
// Group 1: Independent rendering components (can run in parallel)
|
|
5073
5139
|
if (shouldIncludeComponent("canvas", opts) && isCanvasAvailable$1()) {
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
}
|
|
5079
|
-
else {
|
|
5080
|
-
failedComponents.push({
|
|
5081
|
-
component: "canvas",
|
|
5082
|
-
error: "Collection failed or timeout",
|
|
5083
|
-
});
|
|
5084
|
-
}
|
|
5140
|
+
parallelTasks.push(collectComponent("canvas", getCanvasFingerprint, opts).then(result => ({
|
|
5141
|
+
component: "canvas",
|
|
5142
|
+
result
|
|
5143
|
+
})));
|
|
5085
5144
|
}
|
|
5086
|
-
// Collect WebGL fingerprint
|
|
5087
5145
|
if (shouldIncludeComponent("webgl", opts) && isWebGLAvailable()) {
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
}
|
|
5093
|
-
else {
|
|
5094
|
-
failedComponents.push({
|
|
5095
|
-
component: "webgl",
|
|
5096
|
-
error: "Collection failed or timeout",
|
|
5097
|
-
});
|
|
5098
|
-
}
|
|
5146
|
+
parallelTasks.push(collectComponent("webgl", getWebGLFingerprint, opts).then(result => ({
|
|
5147
|
+
component: "webgl",
|
|
5148
|
+
result
|
|
5149
|
+
})));
|
|
5099
5150
|
}
|
|
5100
|
-
// Collect Audio fingerprint
|
|
5101
5151
|
if (shouldIncludeComponent("audio", opts) && isAudioAvailable()) {
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
}
|
|
5107
|
-
else {
|
|
5108
|
-
failedComponents.push({
|
|
5109
|
-
component: "audio",
|
|
5110
|
-
error: "Collection failed or timeout",
|
|
5111
|
-
});
|
|
5112
|
-
}
|
|
5152
|
+
parallelTasks.push(collectComponent("audio", getAudioFingerprint, opts).then(result => ({
|
|
5153
|
+
component: "audio",
|
|
5154
|
+
result
|
|
5155
|
+
})));
|
|
5113
5156
|
}
|
|
5114
|
-
//
|
|
5157
|
+
// Group 2: Fast synchronous components (can run in parallel)
|
|
5115
5158
|
if (shouldIncludeComponent("fonts", opts) && isFontDetectionAvailable()) {
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
}
|
|
5121
|
-
else {
|
|
5122
|
-
failedComponents.push({
|
|
5123
|
-
component: "fonts",
|
|
5124
|
-
error: "Collection failed or timeout",
|
|
5125
|
-
});
|
|
5126
|
-
}
|
|
5159
|
+
parallelTasks.push(collectComponent("fonts", () => getFontFingerprint(!opts.gdprMode), opts).then(result => ({
|
|
5160
|
+
component: "fonts",
|
|
5161
|
+
result
|
|
5162
|
+
})));
|
|
5127
5163
|
}
|
|
5128
|
-
// Collect Screen fingerprint (always available)
|
|
5129
5164
|
if (shouldIncludeComponent("screen", opts) && isScreenAvailable()) {
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
collectedComponents.push("browser");
|
|
5165
|
+
parallelTasks.push(collectComponent("screen", getScreenFingerprint, opts).then(result => ({
|
|
5166
|
+
component: "screen",
|
|
5167
|
+
result
|
|
5168
|
+
})));
|
|
5169
|
+
}
|
|
5170
|
+
if (shouldIncludeComponent("browser", opts) && isBrowserFingerprintingAvailable()) {
|
|
5171
|
+
parallelTasks.push(collectComponent("browser", getBrowserFingerprint, opts).then(result => ({
|
|
5172
|
+
component: "browser",
|
|
5173
|
+
result
|
|
5174
|
+
})));
|
|
5175
|
+
}
|
|
5176
|
+
// Execute all parallel tasks and handle results
|
|
5177
|
+
const parallelResults = await Promise.allSettled(parallelTasks);
|
|
5178
|
+
parallelResults.forEach((promiseResult, index) => {
|
|
5179
|
+
if (promiseResult.status === 'fulfilled' && promiseResult.value.result) {
|
|
5180
|
+
const { component, result } = promiseResult.value;
|
|
5181
|
+
fingerprintData[component] = result;
|
|
5182
|
+
collectedComponents.push(component);
|
|
5149
5183
|
}
|
|
5150
5184
|
else {
|
|
5185
|
+
// Extract component name from the corresponding task
|
|
5186
|
+
const taskComponent = parallelTasks[index];
|
|
5187
|
+
const componentName = taskComponent?.toString().match(/component: "(\w+)"/)?.[1] || 'unknown';
|
|
5151
5188
|
failedComponents.push({
|
|
5152
|
-
component:
|
|
5153
|
-
error:
|
|
5189
|
+
component: componentName,
|
|
5190
|
+
error: promiseResult.status === 'rejected'
|
|
5191
|
+
? promiseResult.reason?.message || "Promise rejected"
|
|
5192
|
+
: "Collection failed or timeout",
|
|
5154
5193
|
});
|
|
5155
5194
|
}
|
|
5156
|
-
}
|
|
5195
|
+
});
|
|
5157
5196
|
// ADDED: Collect Incognito Detection (IMPROVED - 2024)
|
|
5158
5197
|
// Only in enhanced mode for better accuracy
|
|
5159
5198
|
if (!opts.gdprMode) {
|
|
@@ -5180,9 +5219,9 @@ async function collectFingerprint(options = {}) {
|
|
|
5180
5219
|
// Collect Hardware fingerprint (NEW - inspired by FingerprintJS)
|
|
5181
5220
|
if (shouldIncludeComponent("hardware", opts)) {
|
|
5182
5221
|
try {
|
|
5183
|
-
const
|
|
5184
|
-
if (isHardwareDetectionAvailable()) {
|
|
5185
|
-
const result = getHardwareFingerprint();
|
|
5222
|
+
const hardwareModule = await Promise.resolve().then(function () { return hardware; }).catch(() => null);
|
|
5223
|
+
if (hardwareModule?.isHardwareDetectionAvailable?.()) {
|
|
5224
|
+
const result = hardwareModule.getHardwareFingerprint?.();
|
|
5186
5225
|
if (result) {
|
|
5187
5226
|
fingerprintData.hardware = {
|
|
5188
5227
|
value: result,
|
|
@@ -5191,6 +5230,19 @@ async function collectFingerprint(options = {}) {
|
|
|
5191
5230
|
collectedComponents.push("hardware");
|
|
5192
5231
|
}
|
|
5193
5232
|
}
|
|
5233
|
+
else if (!hardwareModule) {
|
|
5234
|
+
// Fallback for when module fails to load
|
|
5235
|
+
const fallbackHardware = {
|
|
5236
|
+
hardwareConcurrency: navigator.hardwareConcurrency || 0,
|
|
5237
|
+
deviceMemory: navigator.deviceMemory || 0,
|
|
5238
|
+
platform: navigator.platform || 'unknown'
|
|
5239
|
+
};
|
|
5240
|
+
fingerprintData.hardware = {
|
|
5241
|
+
value: fallbackHardware,
|
|
5242
|
+
duration: 0,
|
|
5243
|
+
};
|
|
5244
|
+
collectedComponents.push("hardware");
|
|
5245
|
+
}
|
|
5194
5246
|
}
|
|
5195
5247
|
catch (error) {
|
|
5196
5248
|
failedComponents.push({
|
|
@@ -5650,7 +5702,9 @@ async function collectFingerprint(options = {}) {
|
|
|
5650
5702
|
platform: (browser?.platform || "unknown").toLowerCase(),
|
|
5651
5703
|
// PRIORITY 5: Device type (critical for differentiation, ultra-stable)
|
|
5652
5704
|
deviceType,
|
|
5653
|
-
// PRIORITY 6: Enhanced screen resolution
|
|
5705
|
+
// PRIORITY 6: Enhanced screen resolution with exact dimensions for mobile differentiation
|
|
5706
|
+
// For mobile devices, we NEED exact dimensions to differentiate between similar models (S22 vs A54)
|
|
5707
|
+
// For desktop, bucketing is sufficient as hardware varies more
|
|
5654
5708
|
screen: screen
|
|
5655
5709
|
? (() => {
|
|
5656
5710
|
// Use enhanced screen buckets for modern display landscape
|
|
@@ -5745,6 +5799,19 @@ async function collectFingerprint(options = {}) {
|
|
|
5745
5799
|
return "enhanced"; // Slightly enhanced DPI
|
|
5746
5800
|
return "standard"; // Standard 1x displays
|
|
5747
5801
|
};
|
|
5802
|
+
// CRITICAL: Do NOT include exact dimensions in stableCoreVector for mobile!
|
|
5803
|
+
// Reason: Screen dimensions change when:
|
|
5804
|
+
// - Chrome address bar appears/disappears (scroll behavior)
|
|
5805
|
+
// - Virtual keyboard opens/closes
|
|
5806
|
+
// - Screen rotates (portrait ↔ landscape)
|
|
5807
|
+
// - Browser zoom changes
|
|
5808
|
+
// These changes would cause stableCoreHash to change, breaking visitor identification
|
|
5809
|
+
//
|
|
5810
|
+
// INSTEAD: Use ONLY stable screen characteristics (bucket, aspect, density)
|
|
5811
|
+
// For device differentiation (S22 vs A54), use:
|
|
5812
|
+
// - screenFrame (bezel measurements)
|
|
5813
|
+
// - Combination of bucket + hardware + display
|
|
5814
|
+
// - Vector similarity in IdentityResolver
|
|
5748
5815
|
return {
|
|
5749
5816
|
bucket: getScreenBucket(width),
|
|
5750
5817
|
aspectClass: getAspectRatioClass(width, height),
|
|
@@ -5761,6 +5828,11 @@ async function collectFingerprint(options = {}) {
|
|
|
5761
5828
|
: width >= 768
|
|
5762
5829
|
? "tablet"
|
|
5763
5830
|
: "mobile",
|
|
5831
|
+
// ❌ REMOVED: exact dimensions (causes instability on mobile)
|
|
5832
|
+
// Device differentiation is now handled by:
|
|
5833
|
+
// 1. screenFrame (bezel measurements - stable)
|
|
5834
|
+
// 2. hardware + display combination
|
|
5835
|
+
// 3. Vector similarity in IdentityResolver
|
|
5764
5836
|
};
|
|
5765
5837
|
})()
|
|
5766
5838
|
: {
|
|
@@ -5889,7 +5961,18 @@ async function collectFingerprint(options = {}) {
|
|
|
5889
5961
|
: "none",
|
|
5890
5962
|
};
|
|
5891
5963
|
})(),
|
|
5892
|
-
// PRIORITY 8:
|
|
5964
|
+
// PRIORITY 8: Screen frame (bezel measurements) for mobile device differentiation
|
|
5965
|
+
// CRITICAL: This is STABLE across page loads and provides device-specific fingerprint
|
|
5966
|
+
// Samsung S22 vs A54 have different screen frames despite similar resolutions
|
|
5967
|
+
screenFrame: deviceSignals$1?.screenFrame
|
|
5968
|
+
? {
|
|
5969
|
+
top: deviceSignals$1.screenFrame.top || 0,
|
|
5970
|
+
right: deviceSignals$1.screenFrame.right || 0,
|
|
5971
|
+
bottom: deviceSignals$1.screenFrame.bottom || 0,
|
|
5972
|
+
left: deviceSignals$1.screenFrame.left || 0,
|
|
5973
|
+
}
|
|
5974
|
+
: undefined,
|
|
5975
|
+
// PRIORITY 9: Enhanced color and display capabilities bucketing
|
|
5893
5976
|
display: (() => {
|
|
5894
5977
|
const depth = deviceSignals$1.colorDepth;
|
|
5895
5978
|
const gamut = deviceSignals$1.colorGamut;
|
|
@@ -7373,7 +7456,11 @@ class ZaplierSDK {
|
|
|
7373
7456
|
enable: () => {
|
|
7374
7457
|
this.config.heatmap = true;
|
|
7375
7458
|
if (!this.heatmapEngine && this.isInitialized) {
|
|
7376
|
-
|
|
7459
|
+
// Ensure sessionId is available (should be set during initialization)
|
|
7460
|
+
if (!this.sessionId) {
|
|
7461
|
+
this.sessionId = this.generateSessionId();
|
|
7462
|
+
}
|
|
7463
|
+
const sessionId = this.sessionId;
|
|
7377
7464
|
this.heatmapEngine = new HeatmapEngine(sessionId, {
|
|
7378
7465
|
trackClicks: true,
|
|
7379
7466
|
trackScrollDepth: true,
|
|
@@ -7420,7 +7507,11 @@ class ZaplierSDK {
|
|
|
7420
7507
|
enable: () => {
|
|
7421
7508
|
this.config.replay = true;
|
|
7422
7509
|
if (!this.replayEngine && this.isInitialized) {
|
|
7423
|
-
|
|
7510
|
+
// Ensure sessionId is available (should be set during initialization)
|
|
7511
|
+
if (!this.sessionId) {
|
|
7512
|
+
this.sessionId = this.generateSessionId();
|
|
7513
|
+
}
|
|
7514
|
+
const sessionId = this.sessionId;
|
|
7424
7515
|
this.replayEngine = new SessionReplayEngine(sessionId, {
|
|
7425
7516
|
sampleRate: this.config.replaySampling,
|
|
7426
7517
|
maskSensitiveFields: this.config.replayMaskInputs,
|
|
@@ -7444,7 +7535,11 @@ class ZaplierSDK {
|
|
|
7444
7535
|
},
|
|
7445
7536
|
start: () => {
|
|
7446
7537
|
if (!this.replayEngine && this.isInitialized) {
|
|
7447
|
-
|
|
7538
|
+
// Ensure sessionId is available (should be set during initialization)
|
|
7539
|
+
if (!this.sessionId) {
|
|
7540
|
+
this.sessionId = this.generateSessionId();
|
|
7541
|
+
}
|
|
7542
|
+
const sessionId = this.sessionId;
|
|
7448
7543
|
this.replayEngine = new SessionReplayEngine(sessionId, {
|
|
7449
7544
|
sampleRate: 1.0, // Force recording when manually started
|
|
7450
7545
|
maskSensitiveFields: this.config.replayMaskInputs,
|
|
@@ -7534,9 +7629,12 @@ class ZaplierSDK {
|
|
|
7534
7629
|
*/
|
|
7535
7630
|
initializeTrackingEngines() {
|
|
7536
7631
|
try {
|
|
7537
|
-
// Generate session ID
|
|
7632
|
+
// Generate session ID once and only once during initialization
|
|
7538
7633
|
if (!this.sessionId) {
|
|
7539
7634
|
this.sessionId = this.generateSessionId();
|
|
7635
|
+
if (this.config.debug) {
|
|
7636
|
+
console.log("[Zaplier] Generated session ID:", this.sessionId);
|
|
7637
|
+
}
|
|
7540
7638
|
}
|
|
7541
7639
|
// Initialize Session Replay Engine
|
|
7542
7640
|
if (this.config.replay) {
|
|
@@ -7572,7 +7670,33 @@ class ZaplierSDK {
|
|
|
7572
7670
|
* Generate session ID
|
|
7573
7671
|
*/
|
|
7574
7672
|
generateSessionId() {
|
|
7575
|
-
|
|
7673
|
+
// Create deterministic session ID based on page load time and stable browser characteristics
|
|
7674
|
+
const pageLoadTime = performance.timeOrigin || Date.now();
|
|
7675
|
+
const browserFingerprint = this.getBrowserFingerprint();
|
|
7676
|
+
// Use page load time + browser fingerprint for session uniqueness within same page load
|
|
7677
|
+
return pageLoadTime.toString(36) + browserFingerprint.substring(0, 8);
|
|
7678
|
+
}
|
|
7679
|
+
/**
|
|
7680
|
+
* Get stable browser characteristics for session ID generation
|
|
7681
|
+
* Uses only stable, privacy-safe values that don't change during session
|
|
7682
|
+
*/
|
|
7683
|
+
getBrowserFingerprint() {
|
|
7684
|
+
const components = [
|
|
7685
|
+
navigator.userAgent || '',
|
|
7686
|
+
navigator.platform || '',
|
|
7687
|
+
screen.width || 0,
|
|
7688
|
+
screen.height || 0,
|
|
7689
|
+
new Date(2024, 0, 1).getTimezoneOffset(),
|
|
7690
|
+
navigator.language || '',
|
|
7691
|
+
navigator.hardwareConcurrency || 0
|
|
7692
|
+
];
|
|
7693
|
+
const combined = components.join('|');
|
|
7694
|
+
let hash = 0;
|
|
7695
|
+
for (let i = 0; i < combined.length; i++) {
|
|
7696
|
+
const char = combined.charCodeAt(i);
|
|
7697
|
+
hash = ((hash << 5) - hash + char) & 0xffffffff;
|
|
7698
|
+
}
|
|
7699
|
+
return Math.abs(hash).toString(36);
|
|
7576
7700
|
}
|
|
7577
7701
|
/**
|
|
7578
7702
|
* Initialize Anti-Adblock Manager
|
|
@@ -7640,7 +7764,7 @@ class ZaplierSDK {
|
|
|
7640
7764
|
}
|
|
7641
7765
|
// Fallback: collect entropy for diagnostics only; do NOT set local visitorId
|
|
7642
7766
|
try {
|
|
7643
|
-
// Collect
|
|
7767
|
+
// Collect stable entropy sources (no random values for consistency)
|
|
7644
7768
|
const entropyComponents = [
|
|
7645
7769
|
navigator.userAgent || "",
|
|
7646
7770
|
navigator.language || "",
|
|
@@ -7648,44 +7772,40 @@ class ZaplierSDK {
|
|
|
7648
7772
|
screen.width || 0,
|
|
7649
7773
|
screen.height || 0,
|
|
7650
7774
|
screen.colorDepth || 0,
|
|
7651
|
-
new Date().getTimezoneOffset(),
|
|
7652
|
-
// Hardware-specific
|
|
7775
|
+
new Date(2024, 0, 1).getTimezoneOffset(),
|
|
7776
|
+
// Hardware-specific (stable values)
|
|
7653
7777
|
navigator.hardwareConcurrency || 0,
|
|
7654
7778
|
navigator.deviceMemory || 0,
|
|
7655
7779
|
navigator.maxTouchPoints || 0,
|
|
7656
|
-
//
|
|
7657
|
-
performance.
|
|
7658
|
-
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
performance.memory?.
|
|
7664
|
-
Math.random() * 1000000,
|
|
7665
|
-
performance.memory?.totalJSHeapSize ||
|
|
7666
|
-
Math.random() * 1000000,
|
|
7780
|
+
// Use page load time (stable during session) instead of current time
|
|
7781
|
+
performance.timeOrigin || 0,
|
|
7782
|
+
// Deterministic entropy from browser characteristics
|
|
7783
|
+
(navigator.plugins ? navigator.plugins.length : 0),
|
|
7784
|
+
(navigator.mimeTypes ? navigator.mimeTypes.length : 0),
|
|
7785
|
+
// Memory usage pattern (if available, use rounded values for stability)
|
|
7786
|
+
Math.floor((performance.memory?.usedJSHeapSize || 0) / 1000000),
|
|
7787
|
+
Math.floor((performance.memory?.totalJSHeapSize || 0) / 1000000),
|
|
7667
7788
|
// Connection info (if available)
|
|
7668
7789
|
navigator.connection?.effectiveType || "",
|
|
7669
|
-
navigator.connection?.downlink ||
|
|
7790
|
+
Math.floor(navigator.connection?.downlink || 0),
|
|
7670
7791
|
// Additional browser-specific entropy
|
|
7671
7792
|
window.outerHeight || 0,
|
|
7672
7793
|
window.outerWidth || 0,
|
|
7673
|
-
window.devicePixelRatio || 1,
|
|
7674
|
-
//
|
|
7675
|
-
|
|
7794
|
+
Math.floor((window.devicePixelRatio || 1) * 100) / 100, // Round to 2 decimals
|
|
7795
|
+
// Canvas support (deterministic)
|
|
7796
|
+
typeof document.createElement('canvas').getContext === 'function' ? 1 : 0,
|
|
7676
7797
|
];
|
|
7677
|
-
// Create
|
|
7678
|
-
const entropyString = entropyComponents.join("|")
|
|
7679
|
-
//
|
|
7680
|
-
let
|
|
7681
|
-
|
|
7682
|
-
|
|
7683
|
-
window.crypto.getRandomValues(array);
|
|
7684
|
-
cryptoRandom = Array.from(array).join("");
|
|
7798
|
+
// Create deterministic entropy string
|
|
7799
|
+
const entropyString = entropyComponents.join("|");
|
|
7800
|
+
// Add deterministic checksum instead of random values
|
|
7801
|
+
let checksum = 0;
|
|
7802
|
+
for (let i = 0; i < entropyString.length; i++) {
|
|
7803
|
+
checksum = ((checksum << 5) - checksum + entropyString.charCodeAt(i)) & 0xffffffff;
|
|
7685
7804
|
}
|
|
7805
|
+
const deterministic = Math.abs(checksum).toString(36);
|
|
7686
7806
|
// Advanced hash function with better distribution
|
|
7687
7807
|
let hash = 0;
|
|
7688
|
-
const finalString = entropyString +
|
|
7808
|
+
const finalString = entropyString + deterministic;
|
|
7689
7809
|
for (let i = 0; i < finalString.length; i++) {
|
|
7690
7810
|
const char = finalString.charCodeAt(i);
|
|
7691
7811
|
hash = ((hash << 5) - hash + char) & 0xffffffff;
|
|
@@ -10601,8 +10721,8 @@ function getTimezone() {
|
|
|
10601
10721
|
return timezone;
|
|
10602
10722
|
}
|
|
10603
10723
|
}
|
|
10604
|
-
// Fallback: timezone offset
|
|
10605
|
-
const offset = new Date().getTimezoneOffset();
|
|
10724
|
+
// Fallback: timezone offset (STABLE - use standard time to avoid DST changes)
|
|
10725
|
+
const offset = new Date(2024, 0, 1).getTimezoneOffset();
|
|
10606
10726
|
return `UTC${offset > 0 ? '-' : '+'}${Math.abs(offset / 60)}`;
|
|
10607
10727
|
}
|
|
10608
10728
|
catch {
|
|
@@ -12430,16 +12550,84 @@ function getScreenFrame() {
|
|
|
12430
12550
|
}
|
|
12431
12551
|
/**
|
|
12432
12552
|
* Get CPU architecture signature (ARM vs x86)
|
|
12433
|
-
* Uses
|
|
12553
|
+
* Uses multiple detection methods for maximum reliability
|
|
12434
12554
|
*/
|
|
12435
12555
|
function getArchitecture() {
|
|
12436
12556
|
try {
|
|
12437
|
-
//
|
|
12438
|
-
const
|
|
12439
|
-
const
|
|
12440
|
-
|
|
12441
|
-
|
|
12442
|
-
|
|
12557
|
+
// Method 1: Enhanced platform string detection (most reliable)
|
|
12558
|
+
const platform = navigator.platform?.toLowerCase() || '';
|
|
12559
|
+
const userAgent = navigator.userAgent?.toLowerCase() || '';
|
|
12560
|
+
// CRITICAL FIX: Prioritize desktop x86_64 detection first
|
|
12561
|
+
// Windows x64/x86 detection (highest priority for x86_64)
|
|
12562
|
+
if (platform.includes('win') && (userAgent.includes('wow64') || userAgent.includes('x64') || userAgent.includes('x86_64'))) {
|
|
12563
|
+
return 0; // x86_64 - Windows
|
|
12564
|
+
}
|
|
12565
|
+
// Linux x86_64 detection
|
|
12566
|
+
if (platform.includes('linux') && (platform.includes('x86_64') || userAgent.includes('x86_64'))) {
|
|
12567
|
+
return 0; // x86_64 - Linux
|
|
12568
|
+
}
|
|
12569
|
+
// MacOS Intel detection (before ARM check)
|
|
12570
|
+
if (platform.includes('mac') && (platform.includes('intel') || userAgent.includes('intel mac'))) {
|
|
12571
|
+
return 0; // x86_64 - Intel Mac
|
|
12572
|
+
}
|
|
12573
|
+
// Generic x86/x64 detection
|
|
12574
|
+
if (platform.includes('x86') || platform.includes('x64') ||
|
|
12575
|
+
platform.includes('intel') || platform.includes('amd64') ||
|
|
12576
|
+
userAgent.includes('x86') || userAgent.includes('amd64')) {
|
|
12577
|
+
return 0; // x86_64
|
|
12578
|
+
}
|
|
12579
|
+
// ARM detection (after x86_64 checks)
|
|
12580
|
+
if (platform.includes('arm') || userAgent.includes('arm') ||
|
|
12581
|
+
userAgent.includes('aarch64') || platform.includes('aarch64')) {
|
|
12582
|
+
return 255; // ARM
|
|
12583
|
+
}
|
|
12584
|
+
// Method 2: WebGL vendor detection (secondary check)
|
|
12585
|
+
try {
|
|
12586
|
+
const canvas = document.createElement('canvas');
|
|
12587
|
+
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
|
12588
|
+
if (gl && 'getExtension' in gl && 'getParameter' in gl) {
|
|
12589
|
+
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
|
|
12590
|
+
if (debugInfo) {
|
|
12591
|
+
const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL)?.toLowerCase() || '';
|
|
12592
|
+
const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL)?.toLowerCase() || '';
|
|
12593
|
+
// Enhanced x86_64 GPU detection (desktop GPUs)
|
|
12594
|
+
if (vendor.includes('intel') || vendor.includes('nvidia') || vendor.includes('amd') ||
|
|
12595
|
+
renderer.includes('intel') || renderer.includes('nvidia') || renderer.includes('amd') ||
|
|
12596
|
+
renderer.includes('radeon') || renderer.includes('geforce') || renderer.includes('quadro') ||
|
|
12597
|
+
renderer.includes('iris') || renderer.includes('uhd') || renderer.includes('hd graphics')) {
|
|
12598
|
+
return 0; // x86_64 - Desktop GPU
|
|
12599
|
+
}
|
|
12600
|
+
// ARM GPU detection (mobile and ARM devices)
|
|
12601
|
+
if (renderer.includes('mali') || renderer.includes('adreno') ||
|
|
12602
|
+
renderer.includes('powervr') || renderer.includes('tegra') ||
|
|
12603
|
+
renderer.includes('apple gpu') || vendor.includes('arm') ||
|
|
12604
|
+
renderer.includes('videocore')) {
|
|
12605
|
+
return 255; // ARM - Mobile/ARM GPU
|
|
12606
|
+
}
|
|
12607
|
+
}
|
|
12608
|
+
}
|
|
12609
|
+
}
|
|
12610
|
+
catch {
|
|
12611
|
+
// WebGL detection failed, continue
|
|
12612
|
+
}
|
|
12613
|
+
// Method 3: Fallback floating point NaN behavior (least reliable)
|
|
12614
|
+
try {
|
|
12615
|
+
const f = new Float32Array(1);
|
|
12616
|
+
const u8 = new Uint8Array(f.buffer);
|
|
12617
|
+
f[0] = Infinity;
|
|
12618
|
+
f[0] = f[0] - f[0]; // Should produce NaN
|
|
12619
|
+
const nanBehavior = u8[3];
|
|
12620
|
+
// Only trust this if platform detection was inconclusive
|
|
12621
|
+
// AND we have strong indicators from other methods
|
|
12622
|
+
if (platform.includes('linux') && nanBehavior === 255) {
|
|
12623
|
+
// Linux on ARM is less common, but validate with other signals
|
|
12624
|
+
return nanBehavior;
|
|
12625
|
+
}
|
|
12626
|
+
return nanBehavior;
|
|
12627
|
+
}
|
|
12628
|
+
catch {
|
|
12629
|
+
return undefined;
|
|
12630
|
+
}
|
|
12443
12631
|
}
|
|
12444
12632
|
catch {
|
|
12445
12633
|
return undefined;
|