@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.d.ts CHANGED
@@ -1289,7 +1289,7 @@ declare function hash32(input: string): string;
1289
1289
  /**
1290
1290
  * Hash fingerprint components into a stable identifier
1291
1291
  */
1292
- declare function hashFingerprint(components: Record<string, any>): string;
1292
+ declare function hashFingerprint(components: Record<string, any>, debug?: boolean): string;
1293
1293
  /**
1294
1294
  * Generate a visitor ID from fingerprint hash
1295
1295
  */
package/dist/index.esm.js CHANGED
@@ -151,8 +151,12 @@ function x64hash128(input, seed = 0) {
151
151
  h1 = x64Add(h1, h2);
152
152
  h2 = x64Add(h2, h1);
153
153
  // Convert to hex string
154
- const hex1 = h1[0].toString(16).padStart(8, "0") + h1[1].toString(16).padStart(8, "0");
155
- const hex2 = h2[0].toString(16).padStart(8, "0") + h2[1].toString(16).padStart(8, "0");
154
+ // CRITICAL: Use >>> 0 to convert to unsigned 32-bit integer before converting to hex
155
+ // This prevents negative numbers from generating hashes with minus signs (e.g., "-6b43973f")
156
+ const hex1 = (h1[0] >>> 0).toString(16).padStart(8, "0") +
157
+ (h1[1] >>> 0).toString(16).padStart(8, "0");
158
+ const hex2 = (h2[0] >>> 0).toString(16).padStart(8, "0") +
159
+ (h2[1] >>> 0).toString(16).padStart(8, "0");
156
160
  return hex1 + hex2;
157
161
  }
158
162
  /**
@@ -172,13 +176,26 @@ function hash32(input) {
172
176
  /**
173
177
  * Hash fingerprint components into a stable identifier
174
178
  */
175
- function hashFingerprint(components) {
179
+ function hashFingerprint(components, debug = false) {
176
180
  // Convert components to canonical string representation using deep sorting
177
181
  // CRITICAL: Do NOT use JSON.stringify(obj, keys) as it filters out nested properties!
178
182
  // Use canonicalizeStable instead which handles deep sorting and normalization.
179
- const canonical = JSON.stringify(canonicalizeStable(components));
183
+ const canonicalized = canonicalizeStable(components);
184
+ const canonical = JSON.stringify(canonicalized);
185
+ if (debug) {
186
+ console.log("[RabbitTracker] hashFingerprint debug:", {
187
+ originalKeys: Object.keys(components),
188
+ canonicalizedKeys: Object.keys(canonicalized),
189
+ canonicalLength: canonical.length,
190
+ sample: canonical.substring(0, 200) + "...",
191
+ });
192
+ }
180
193
  // Use MurmurHash3 x64 for consistent hashing
181
- return x64hash128(canonical);
194
+ const hash = x64hash128(canonical);
195
+ if (debug) {
196
+ console.log("[RabbitTracker] Generated fingerprintHash:", hash.substring(0, 32));
197
+ }
198
+ return hash;
182
199
  }
183
200
  /**
184
201
  * Generate a visitor ID from fingerprint hash
@@ -187,8 +204,33 @@ function generateVisitorId(fingerprintHash) {
187
204
  // Use first 16 characters of the hash for visitor ID
188
205
  return "vis_" + fingerprintHash.substring(0, 16);
189
206
  }
207
+ /**
208
+ * Check if a key represents an unstable component that should be excluded from hashing
209
+ */
210
+ function isUnstableKey(key) {
211
+ // Unstable keys that change between requests:
212
+ // - duration: collection time varies
213
+ // - timestamp: changes every request
214
+ // - error: error messages can vary
215
+ // - errors: error arrays can vary
216
+ // - collectionTime: varies per request
217
+ const unstableKeys = [
218
+ "duration",
219
+ "timestamp",
220
+ "error",
221
+ "errors",
222
+ "collectionTime",
223
+ "startTime",
224
+ "endTime",
225
+ "_timestamp", // internal timestamp fields
226
+ ];
227
+ return (unstableKeys.includes(key) ||
228
+ key.endsWith("_timestamp") ||
229
+ key.endsWith("Timestamp"));
230
+ }
190
231
  /**
191
232
  * Canonicalize an object deeply with sorted keys and normalized primitives
233
+ * CRITICAL: Filters out unstable components that vary between requests
192
234
  */
193
235
  function canonicalizeStable(input) {
194
236
  if (input === null || input === undefined)
@@ -203,6 +245,10 @@ function canonicalizeStable(input) {
203
245
  const keys = Object.keys(input).sort();
204
246
  const out = {};
205
247
  for (const k of keys) {
248
+ // CRITICAL: Skip unstable keys that cause hash instability
249
+ if (isUnstableKey(k)) {
250
+ continue;
251
+ }
206
252
  const v = canonicalizeStable(input[k]);
207
253
  if (v !== null)
208
254
  out[k] = v;
@@ -223,9 +269,22 @@ function canonicalizeStable(input) {
223
269
  /**
224
270
  * Compute stable core hash from a minimal, invariant vector
225
271
  */
226
- function hashStableCore(coreVector) {
227
- const canonical = JSON.stringify(canonicalizeStable(coreVector));
228
- return x64hash128(canonical);
272
+ function hashStableCore(coreVector, debug = false) {
273
+ const canonicalized = canonicalizeStable(coreVector);
274
+ const canonical = JSON.stringify(canonicalized);
275
+ if (debug) {
276
+ console.log("[RabbitTracker] hashStableCore debug:", {
277
+ originalKeys: Object.keys(coreVector),
278
+ canonicalizedKeys: Object.keys(canonicalized),
279
+ canonicalLength: canonical.length,
280
+ sample: canonical.substring(0, 200) + "...",
281
+ });
282
+ }
283
+ const hash = x64hash128(canonical);
284
+ if (debug) {
285
+ console.log("[RabbitTracker] Generated stableCoreHash:", hash.substring(0, 32));
286
+ }
287
+ return hash;
229
288
  }
230
289
 
231
290
  /**
@@ -4936,7 +4995,7 @@ async function collectComponent(componentType, collector, options) {
4936
4995
  "system",
4937
4996
  ].includes(componentType);
4938
4997
  // Reduce retries for WebGL to prevent context leaks
4939
- const maxRetries = isWebGLComponent ? 2 : (isCriticalComponent ? 3 : 1);
4998
+ const maxRetries = isWebGLComponent ? 2 : isCriticalComponent ? 3 : 1;
4940
4999
  const retryDelays = isWebGLComponent ? [200, 500] : [100, 250, 500]; // Longer delays for WebGL
4941
5000
  for (let attempt = 0; attempt < maxRetries; attempt++) {
4942
5001
  try {
@@ -5568,7 +5627,9 @@ async function collectFingerprint(options = {}) {
5568
5627
  // Fallback to legacy detection if imports fail
5569
5628
  const ua = uaStr;
5570
5629
  const m = ua.match(/(chrome|safari|firefox|edge|edg|brave|opera|opr|chromium)/i);
5571
- const family = (m?.[1] || ua.split(" ")[0] || "unknown").toLowerCase();
5630
+ const family = (m?.[1] ||
5631
+ ua.split(" ")[0] ||
5632
+ "unknown").toLowerCase();
5572
5633
  return family;
5573
5634
  }
5574
5635
  })(),
@@ -5576,8 +5637,10 @@ async function collectFingerprint(options = {}) {
5576
5637
  timezone: browser?.timezone || "unknown",
5577
5638
  // PRIORITY 3: Primary language only (most stable language preference)
5578
5639
  // Remove secondary languages as they can change more frequently
5579
- primaryLanguage: Array.isArray(browser?.languages) && browser.languages.length > 0 && browser.languages[0]
5580
- ? browser.languages[0].toLowerCase().split('-')[0] // Only language code (en, pt, fr), not region
5640
+ primaryLanguage: Array.isArray(browser?.languages) &&
5641
+ browser.languages.length > 0 &&
5642
+ browser.languages[0]
5643
+ ? browser.languages[0].toLowerCase().split("-")[0] // Only language code (en, pt, fr), not region
5581
5644
  : "unknown",
5582
5645
  // PRIORITY 4: Platform (ultra-stable - OS doesn't change)
5583
5646
  platform: (browser?.platform || "unknown").toLowerCase(),
@@ -5685,11 +5748,24 @@ async function collectFingerprint(options = {}) {
5685
5748
  // Simplified ratio for matching (rounded to prevent micro-variations)
5686
5749
  ratio: height > 0 ? Math.round((width / height) * 100) / 100 : 0,
5687
5750
  // Size category for general device classification
5688
- sizeCategory: width >= 2560 ? "large" : width >= 1920 ? "desktop" :
5689
- width >= 1024 ? "laptop" : width >= 768 ? "tablet" : "mobile"
5751
+ sizeCategory: width >= 2560
5752
+ ? "large"
5753
+ : width >= 1920
5754
+ ? "desktop"
5755
+ : width >= 1024
5756
+ ? "laptop"
5757
+ : width >= 768
5758
+ ? "tablet"
5759
+ : "mobile",
5690
5760
  };
5691
5761
  })()
5692
- : { bucket: "unknown", aspectClass: "unknown", densityClass: "unknown", ratio: 0, sizeCategory: "unknown" },
5762
+ : {
5763
+ bucket: "unknown",
5764
+ aspectClass: "unknown",
5765
+ densityClass: "unknown",
5766
+ ratio: 0,
5767
+ sizeCategory: "unknown",
5768
+ },
5693
5769
  // CRITICAL: Do NOT include availability flags (canvas/webgl/audio) in stableCoreVector
5694
5770
  // These flags can change between requests due to timeouts, permissions, or hardware issues
5695
5771
  // and would cause the stableCoreHash to change, breaking visitor identification
@@ -5800,10 +5876,13 @@ async function collectFingerprint(options = {}) {
5800
5876
  arch: archBucket,
5801
5877
  class: getHardwareClass(coresBucket, memoryBucket),
5802
5878
  // Touch capability for mobile/desktop differentiation
5803
- touch: deviceSignals$1.touchSupport?.maxTouchPoints ?
5804
- (deviceSignals$1.touchSupport.maxTouchPoints >= 10 ? "multi" :
5805
- deviceSignals$1.touchSupport.maxTouchPoints >= 5 ? "standard" : "basic") :
5806
- "none"
5879
+ touch: deviceSignals$1.touchSupport?.maxTouchPoints
5880
+ ? deviceSignals$1.touchSupport.maxTouchPoints >= 10
5881
+ ? "multi"
5882
+ : deviceSignals$1.touchSupport.maxTouchPoints >= 5
5883
+ ? "standard"
5884
+ : "basic"
5885
+ : "none",
5807
5886
  };
5808
5887
  })(),
5809
5888
  // PRIORITY 8: Enhanced color and display capabilities bucketing
@@ -5829,10 +5908,14 @@ async function collectFingerprint(options = {}) {
5829
5908
  if (!gamut)
5830
5909
  return "unknown";
5831
5910
  switch (gamut) {
5832
- case "rec2020": return "professional"; // Professional/HDR displays
5833
- case "p3": return "enhanced"; // Modern displays (Apple, etc.)
5834
- case "srgb": return "standard"; // Standard displays
5835
- default: return "unknown";
5911
+ case "rec2020":
5912
+ return "professional"; // Professional/HDR displays
5913
+ case "p3":
5914
+ return "enhanced"; // Modern displays (Apple, etc.)
5915
+ case "srgb":
5916
+ return "standard"; // Standard displays
5917
+ default:
5918
+ return "unknown";
5836
5919
  }
5837
5920
  };
5838
5921
  // Combined display quality classification
@@ -5856,7 +5939,7 @@ async function collectFingerprint(options = {}) {
5856
5939
  return {
5857
5940
  depth: depthClass,
5858
5941
  gamut: gamutClass,
5859
- class: getDisplayClass(depthClass, gamutClass)
5942
+ class: getDisplayClass(depthClass, gamutClass),
5860
5943
  };
5861
5944
  })(),
5862
5945
  };
@@ -5875,7 +5958,9 @@ async function collectFingerprint(options = {}) {
5875
5958
  // Add a simple checksum for additional uniqueness (deterministic)
5876
5959
  let checksum = 0;
5877
5960
  for (let i = 0; i < entropyString.length; i++) {
5878
- checksum = ((checksum << 5) - checksum + entropyString.charCodeAt(i)) & 0xffffffff;
5961
+ checksum =
5962
+ ((checksum << 5) - checksum + entropyString.charCodeAt(i)) &
5963
+ 0xffffffff;
5879
5964
  }
5880
5965
  coreVector._entropy = Math.abs(checksum).toString(36).substring(0, 8);
5881
5966
  }
@@ -5891,7 +5976,7 @@ async function collectFingerprint(options = {}) {
5891
5976
  // - Vendor flavors (can change)
5892
5977
  // - Exact hardware values (use buckets)
5893
5978
  // Generate ultra-stable hash with additional collision resistance
5894
- const stableCoreHash = hashStableCore(coreVector);
5979
+ const stableCoreHash = hashStableCore(coreVector, opts.debug);
5895
5980
  // CRITICAL: Generate enhanced fingerprint hash combining unstable + stable components
5896
5981
  // This prevents collisions by adding stable entropy to the fingerprint hash
5897
5982
  const enhancedComponentValues = {
@@ -5900,7 +5985,7 @@ async function collectFingerprint(options = {}) {
5900
5985
  // REMOVED: _hourlyEntropy - was causing visitor ID instability (same user = different IDs)
5901
5986
  };
5902
5987
  // Generate collision-resistant fingerprint hash
5903
- const enhancedHash = hashFingerprint(enhancedComponentValues);
5988
+ const enhancedHash = hashFingerprint(enhancedComponentValues, opts.debug);
5904
5989
  // Update finalData with the enhanced hash
5905
5990
  finalData.hash = enhancedHash;
5906
5991
  // Log stable core components for debugging collision issues