@zaplier/sdk 1.0.5 → 1.0.7

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 CHANGED
@@ -155,8 +155,12 @@ function x64hash128(input, seed = 0) {
155
155
  h1 = x64Add(h1, h2);
156
156
  h2 = x64Add(h2, h1);
157
157
  // Convert to hex string
158
- const hex1 = h1[0].toString(16).padStart(8, "0") + h1[1].toString(16).padStart(8, "0");
159
- const hex2 = h2[0].toString(16).padStart(8, "0") + h2[1].toString(16).padStart(8, "0");
158
+ // CRITICAL: Use >>> 0 to convert to unsigned 32-bit integer before converting to hex
159
+ // This prevents negative numbers from generating hashes with minus signs (e.g., "-6b43973f")
160
+ const hex1 = (h1[0] >>> 0).toString(16).padStart(8, "0") +
161
+ (h1[1] >>> 0).toString(16).padStart(8, "0");
162
+ const hex2 = (h2[0] >>> 0).toString(16).padStart(8, "0") +
163
+ (h2[1] >>> 0).toString(16).padStart(8, "0");
160
164
  return hex1 + hex2;
161
165
  }
162
166
  /**
@@ -176,13 +180,26 @@ function hash32(input) {
176
180
  /**
177
181
  * Hash fingerprint components into a stable identifier
178
182
  */
179
- function hashFingerprint(components) {
183
+ function hashFingerprint(components, debug = false) {
180
184
  // Convert components to canonical string representation using deep sorting
181
185
  // CRITICAL: Do NOT use JSON.stringify(obj, keys) as it filters out nested properties!
182
186
  // Use canonicalizeStable instead which handles deep sorting and normalization.
183
- const canonical = JSON.stringify(canonicalizeStable(components));
187
+ const canonicalized = canonicalizeStable(components);
188
+ const canonical = JSON.stringify(canonicalized);
189
+ if (debug) {
190
+ console.log("[RabbitTracker] hashFingerprint debug:", {
191
+ originalKeys: Object.keys(components),
192
+ canonicalizedKeys: Object.keys(canonicalized),
193
+ canonicalLength: canonical.length,
194
+ sample: canonical.substring(0, 200) + "...",
195
+ });
196
+ }
184
197
  // Use MurmurHash3 x64 for consistent hashing
185
- return x64hash128(canonical);
198
+ const hash = x64hash128(canonical);
199
+ if (debug) {
200
+ console.log("[RabbitTracker] Generated fingerprintHash:", hash.substring(0, 32));
201
+ }
202
+ return hash;
186
203
  }
187
204
  /**
188
205
  * Generate a visitor ID from fingerprint hash
@@ -191,8 +208,33 @@ function generateVisitorId(fingerprintHash) {
191
208
  // Use first 16 characters of the hash for visitor ID
192
209
  return "vis_" + fingerprintHash.substring(0, 16);
193
210
  }
211
+ /**
212
+ * Check if a key represents an unstable component that should be excluded from hashing
213
+ */
214
+ function isUnstableKey(key) {
215
+ // Unstable keys that change between requests:
216
+ // - duration: collection time varies
217
+ // - timestamp: changes every request
218
+ // - error: error messages can vary
219
+ // - errors: error arrays can vary
220
+ // - collectionTime: varies per request
221
+ const unstableKeys = [
222
+ "duration",
223
+ "timestamp",
224
+ "error",
225
+ "errors",
226
+ "collectionTime",
227
+ "startTime",
228
+ "endTime",
229
+ "_timestamp", // internal timestamp fields
230
+ ];
231
+ return (unstableKeys.includes(key) ||
232
+ key.endsWith("_timestamp") ||
233
+ key.endsWith("Timestamp"));
234
+ }
194
235
  /**
195
236
  * Canonicalize an object deeply with sorted keys and normalized primitives
237
+ * CRITICAL: Filters out unstable components that vary between requests
196
238
  */
197
239
  function canonicalizeStable(input) {
198
240
  if (input === null || input === undefined)
@@ -207,6 +249,10 @@ function canonicalizeStable(input) {
207
249
  const keys = Object.keys(input).sort();
208
250
  const out = {};
209
251
  for (const k of keys) {
252
+ // CRITICAL: Skip unstable keys that cause hash instability
253
+ if (isUnstableKey(k)) {
254
+ continue;
255
+ }
210
256
  const v = canonicalizeStable(input[k]);
211
257
  if (v !== null)
212
258
  out[k] = v;
@@ -227,9 +273,22 @@ function canonicalizeStable(input) {
227
273
  /**
228
274
  * Compute stable core hash from a minimal, invariant vector
229
275
  */
230
- function hashStableCore(coreVector) {
231
- const canonical = JSON.stringify(canonicalizeStable(coreVector));
232
- return x64hash128(canonical);
276
+ function hashStableCore(coreVector, debug = false) {
277
+ const canonicalized = canonicalizeStable(coreVector);
278
+ const canonical = JSON.stringify(canonicalized);
279
+ if (debug) {
280
+ console.log("[RabbitTracker] hashStableCore debug:", {
281
+ originalKeys: Object.keys(coreVector),
282
+ canonicalizedKeys: Object.keys(canonicalized),
283
+ canonicalLength: canonical.length,
284
+ sample: canonical.substring(0, 200) + "...",
285
+ });
286
+ }
287
+ const hash = x64hash128(canonical);
288
+ if (debug) {
289
+ console.log("[RabbitTracker] Generated stableCoreHash:", hash.substring(0, 32));
290
+ }
291
+ return hash;
233
292
  }
234
293
 
235
294
  /**
@@ -4940,7 +4999,7 @@ async function collectComponent(componentType, collector, options) {
4940
4999
  "system",
4941
5000
  ].includes(componentType);
4942
5001
  // Reduce retries for WebGL to prevent context leaks
4943
- const maxRetries = isWebGLComponent ? 2 : (isCriticalComponent ? 3 : 1);
5002
+ const maxRetries = isWebGLComponent ? 2 : isCriticalComponent ? 3 : 1;
4944
5003
  const retryDelays = isWebGLComponent ? [200, 500] : [100, 250, 500]; // Longer delays for WebGL
4945
5004
  for (let attempt = 0; attempt < maxRetries; attempt++) {
4946
5005
  try {
@@ -5572,7 +5631,9 @@ async function collectFingerprint(options = {}) {
5572
5631
  // Fallback to legacy detection if imports fail
5573
5632
  const ua = uaStr;
5574
5633
  const m = ua.match(/(chrome|safari|firefox|edge|edg|brave|opera|opr|chromium)/i);
5575
- const family = (m?.[1] || ua.split(" ")[0] || "unknown").toLowerCase();
5634
+ const family = (m?.[1] ||
5635
+ ua.split(" ")[0] ||
5636
+ "unknown").toLowerCase();
5576
5637
  return family;
5577
5638
  }
5578
5639
  })(),
@@ -5580,8 +5641,10 @@ async function collectFingerprint(options = {}) {
5580
5641
  timezone: browser?.timezone || "unknown",
5581
5642
  // PRIORITY 3: Primary language only (most stable language preference)
5582
5643
  // Remove secondary languages as they can change more frequently
5583
- primaryLanguage: Array.isArray(browser?.languages) && browser.languages.length > 0 && browser.languages[0]
5584
- ? browser.languages[0].toLowerCase().split('-')[0] // Only language code (en, pt, fr), not region
5644
+ primaryLanguage: Array.isArray(browser?.languages) &&
5645
+ browser.languages.length > 0 &&
5646
+ browser.languages[0]
5647
+ ? browser.languages[0].toLowerCase().split("-")[0] // Only language code (en, pt, fr), not region
5585
5648
  : "unknown",
5586
5649
  // PRIORITY 4: Platform (ultra-stable - OS doesn't change)
5587
5650
  platform: (browser?.platform || "unknown").toLowerCase(),
@@ -5689,11 +5752,24 @@ async function collectFingerprint(options = {}) {
5689
5752
  // Simplified ratio for matching (rounded to prevent micro-variations)
5690
5753
  ratio: height > 0 ? Math.round((width / height) * 100) / 100 : 0,
5691
5754
  // Size category for general device classification
5692
- sizeCategory: width >= 2560 ? "large" : width >= 1920 ? "desktop" :
5693
- width >= 1024 ? "laptop" : width >= 768 ? "tablet" : "mobile"
5755
+ sizeCategory: width >= 2560
5756
+ ? "large"
5757
+ : width >= 1920
5758
+ ? "desktop"
5759
+ : width >= 1024
5760
+ ? "laptop"
5761
+ : width >= 768
5762
+ ? "tablet"
5763
+ : "mobile",
5694
5764
  };
5695
5765
  })()
5696
- : { bucket: "unknown", aspectClass: "unknown", densityClass: "unknown", ratio: 0, sizeCategory: "unknown" },
5766
+ : {
5767
+ bucket: "unknown",
5768
+ aspectClass: "unknown",
5769
+ densityClass: "unknown",
5770
+ ratio: 0,
5771
+ sizeCategory: "unknown",
5772
+ },
5697
5773
  // CRITICAL: Do NOT include availability flags (canvas/webgl/audio) in stableCoreVector
5698
5774
  // These flags can change between requests due to timeouts, permissions, or hardware issues
5699
5775
  // and would cause the stableCoreHash to change, breaking visitor identification
@@ -5804,10 +5880,13 @@ async function collectFingerprint(options = {}) {
5804
5880
  arch: archBucket,
5805
5881
  class: getHardwareClass(coresBucket, memoryBucket),
5806
5882
  // Touch capability for mobile/desktop differentiation
5807
- touch: deviceSignals$1.touchSupport?.maxTouchPoints ?
5808
- (deviceSignals$1.touchSupport.maxTouchPoints >= 10 ? "multi" :
5809
- deviceSignals$1.touchSupport.maxTouchPoints >= 5 ? "standard" : "basic") :
5810
- "none"
5883
+ touch: deviceSignals$1.touchSupport?.maxTouchPoints
5884
+ ? deviceSignals$1.touchSupport.maxTouchPoints >= 10
5885
+ ? "multi"
5886
+ : deviceSignals$1.touchSupport.maxTouchPoints >= 5
5887
+ ? "standard"
5888
+ : "basic"
5889
+ : "none",
5811
5890
  };
5812
5891
  })(),
5813
5892
  // PRIORITY 8: Enhanced color and display capabilities bucketing
@@ -5833,10 +5912,14 @@ async function collectFingerprint(options = {}) {
5833
5912
  if (!gamut)
5834
5913
  return "unknown";
5835
5914
  switch (gamut) {
5836
- case "rec2020": return "professional"; // Professional/HDR displays
5837
- case "p3": return "enhanced"; // Modern displays (Apple, etc.)
5838
- case "srgb": return "standard"; // Standard displays
5839
- default: return "unknown";
5915
+ case "rec2020":
5916
+ return "professional"; // Professional/HDR displays
5917
+ case "p3":
5918
+ return "enhanced"; // Modern displays (Apple, etc.)
5919
+ case "srgb":
5920
+ return "standard"; // Standard displays
5921
+ default:
5922
+ return "unknown";
5840
5923
  }
5841
5924
  };
5842
5925
  // Combined display quality classification
@@ -5860,7 +5943,7 @@ async function collectFingerprint(options = {}) {
5860
5943
  return {
5861
5944
  depth: depthClass,
5862
5945
  gamut: gamutClass,
5863
- class: getDisplayClass(depthClass, gamutClass)
5946
+ class: getDisplayClass(depthClass, gamutClass),
5864
5947
  };
5865
5948
  })(),
5866
5949
  };
@@ -5879,7 +5962,9 @@ async function collectFingerprint(options = {}) {
5879
5962
  // Add a simple checksum for additional uniqueness (deterministic)
5880
5963
  let checksum = 0;
5881
5964
  for (let i = 0; i < entropyString.length; i++) {
5882
- checksum = ((checksum << 5) - checksum + entropyString.charCodeAt(i)) & 0xffffffff;
5965
+ checksum =
5966
+ ((checksum << 5) - checksum + entropyString.charCodeAt(i)) &
5967
+ 0xffffffff;
5883
5968
  }
5884
5969
  coreVector._entropy = Math.abs(checksum).toString(36).substring(0, 8);
5885
5970
  }
@@ -5895,7 +5980,7 @@ async function collectFingerprint(options = {}) {
5895
5980
  // - Vendor flavors (can change)
5896
5981
  // - Exact hardware values (use buckets)
5897
5982
  // Generate ultra-stable hash with additional collision resistance
5898
- const stableCoreHash = hashStableCore(coreVector);
5983
+ const stableCoreHash = hashStableCore(coreVector, opts.debug);
5899
5984
  // CRITICAL: Generate enhanced fingerprint hash combining unstable + stable components
5900
5985
  // This prevents collisions by adding stable entropy to the fingerprint hash
5901
5986
  const enhancedComponentValues = {
@@ -5904,7 +5989,7 @@ async function collectFingerprint(options = {}) {
5904
5989
  // REMOVED: _hourlyEntropy - was causing visitor ID instability (same user = different IDs)
5905
5990
  };
5906
5991
  // Generate collision-resistant fingerprint hash
5907
- const enhancedHash = hashFingerprint(enhancedComponentValues);
5992
+ const enhancedHash = hashFingerprint(enhancedComponentValues, opts.debug);
5908
5993
  // Update finalData with the enhanced hash
5909
5994
  finalData.hash = enhancedHash;
5910
5995
  // Log stable core components for debugging collision issues