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