@zaplier/sdk 1.0.4 → 1.0.6

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
@@ -172,13 +172,26 @@ function hash32(input) {
172
172
  /**
173
173
  * Hash fingerprint components into a stable identifier
174
174
  */
175
- function hashFingerprint(components) {
175
+ function hashFingerprint(components, debug = false) {
176
176
  // Convert components to canonical string representation using deep sorting
177
177
  // CRITICAL: Do NOT use JSON.stringify(obj, keys) as it filters out nested properties!
178
178
  // Use canonicalizeStable instead which handles deep sorting and normalization.
179
- const canonical = JSON.stringify(canonicalizeStable(components));
179
+ const canonicalized = canonicalizeStable(components);
180
+ const canonical = JSON.stringify(canonicalized);
181
+ if (debug) {
182
+ console.log("[RabbitTracker] hashFingerprint debug:", {
183
+ originalKeys: Object.keys(components),
184
+ canonicalizedKeys: Object.keys(canonicalized),
185
+ canonicalLength: canonical.length,
186
+ sample: canonical.substring(0, 200) + "...",
187
+ });
188
+ }
180
189
  // Use MurmurHash3 x64 for consistent hashing
181
- return x64hash128(canonical);
190
+ const hash = x64hash128(canonical);
191
+ if (debug) {
192
+ console.log("[RabbitTracker] Generated fingerprintHash:", hash.substring(0, 32));
193
+ }
194
+ return hash;
182
195
  }
183
196
  /**
184
197
  * Generate a visitor ID from fingerprint hash
@@ -187,8 +200,33 @@ function generateVisitorId(fingerprintHash) {
187
200
  // Use first 16 characters of the hash for visitor ID
188
201
  return "vis_" + fingerprintHash.substring(0, 16);
189
202
  }
203
+ /**
204
+ * Check if a key represents an unstable component that should be excluded from hashing
205
+ */
206
+ function isUnstableKey(key) {
207
+ // Unstable keys that change between requests:
208
+ // - duration: collection time varies
209
+ // - timestamp: changes every request
210
+ // - error: error messages can vary
211
+ // - errors: error arrays can vary
212
+ // - collectionTime: varies per request
213
+ const unstableKeys = [
214
+ "duration",
215
+ "timestamp",
216
+ "error",
217
+ "errors",
218
+ "collectionTime",
219
+ "startTime",
220
+ "endTime",
221
+ "_timestamp", // internal timestamp fields
222
+ ];
223
+ return (unstableKeys.includes(key) ||
224
+ key.endsWith("_timestamp") ||
225
+ key.endsWith("Timestamp"));
226
+ }
190
227
  /**
191
228
  * Canonicalize an object deeply with sorted keys and normalized primitives
229
+ * CRITICAL: Filters out unstable components that vary between requests
192
230
  */
193
231
  function canonicalizeStable(input) {
194
232
  if (input === null || input === undefined)
@@ -203,6 +241,10 @@ function canonicalizeStable(input) {
203
241
  const keys = Object.keys(input).sort();
204
242
  const out = {};
205
243
  for (const k of keys) {
244
+ // CRITICAL: Skip unstable keys that cause hash instability
245
+ if (isUnstableKey(k)) {
246
+ continue;
247
+ }
206
248
  const v = canonicalizeStable(input[k]);
207
249
  if (v !== null)
208
250
  out[k] = v;
@@ -223,9 +265,22 @@ function canonicalizeStable(input) {
223
265
  /**
224
266
  * Compute stable core hash from a minimal, invariant vector
225
267
  */
226
- function hashStableCore(coreVector) {
227
- const canonical = JSON.stringify(canonicalizeStable(coreVector));
228
- return x64hash128(canonical);
268
+ function hashStableCore(coreVector, debug = false) {
269
+ const canonicalized = canonicalizeStable(coreVector);
270
+ const canonical = JSON.stringify(canonicalized);
271
+ if (debug) {
272
+ console.log("[RabbitTracker] hashStableCore debug:", {
273
+ originalKeys: Object.keys(coreVector),
274
+ canonicalizedKeys: Object.keys(canonicalized),
275
+ canonicalLength: canonical.length,
276
+ sample: canonical.substring(0, 200) + "...",
277
+ });
278
+ }
279
+ const hash = x64hash128(canonical);
280
+ if (debug) {
281
+ console.log("[RabbitTracker] Generated stableCoreHash:", hash.substring(0, 32));
282
+ }
283
+ return hash;
229
284
  }
230
285
 
231
286
  /**
@@ -4936,7 +4991,7 @@ async function collectComponent(componentType, collector, options) {
4936
4991
  "system",
4937
4992
  ].includes(componentType);
4938
4993
  // Reduce retries for WebGL to prevent context leaks
4939
- const maxRetries = isWebGLComponent ? 2 : (isCriticalComponent ? 3 : 1);
4994
+ const maxRetries = isWebGLComponent ? 2 : isCriticalComponent ? 3 : 1;
4940
4995
  const retryDelays = isWebGLComponent ? [200, 500] : [100, 250, 500]; // Longer delays for WebGL
4941
4996
  for (let attempt = 0; attempt < maxRetries; attempt++) {
4942
4997
  try {
@@ -5568,7 +5623,9 @@ async function collectFingerprint(options = {}) {
5568
5623
  // Fallback to legacy detection if imports fail
5569
5624
  const ua = uaStr;
5570
5625
  const m = ua.match(/(chrome|safari|firefox|edge|edg|brave|opera|opr|chromium)/i);
5571
- const family = (m?.[1] || ua.split(" ")[0] || "unknown").toLowerCase();
5626
+ const family = (m?.[1] ||
5627
+ ua.split(" ")[0] ||
5628
+ "unknown").toLowerCase();
5572
5629
  return family;
5573
5630
  }
5574
5631
  })(),
@@ -5576,8 +5633,10 @@ async function collectFingerprint(options = {}) {
5576
5633
  timezone: browser?.timezone || "unknown",
5577
5634
  // PRIORITY 3: Primary language only (most stable language preference)
5578
5635
  // 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
5636
+ primaryLanguage: Array.isArray(browser?.languages) &&
5637
+ browser.languages.length > 0 &&
5638
+ browser.languages[0]
5639
+ ? browser.languages[0].toLowerCase().split("-")[0] // Only language code (en, pt, fr), not region
5581
5640
  : "unknown",
5582
5641
  // PRIORITY 4: Platform (ultra-stable - OS doesn't change)
5583
5642
  platform: (browser?.platform || "unknown").toLowerCase(),
@@ -5685,11 +5744,24 @@ async function collectFingerprint(options = {}) {
5685
5744
  // Simplified ratio for matching (rounded to prevent micro-variations)
5686
5745
  ratio: height > 0 ? Math.round((width / height) * 100) / 100 : 0,
5687
5746
  // Size category for general device classification
5688
- sizeCategory: width >= 2560 ? "large" : width >= 1920 ? "desktop" :
5689
- width >= 1024 ? "laptop" : width >= 768 ? "tablet" : "mobile"
5747
+ sizeCategory: width >= 2560
5748
+ ? "large"
5749
+ : width >= 1920
5750
+ ? "desktop"
5751
+ : width >= 1024
5752
+ ? "laptop"
5753
+ : width >= 768
5754
+ ? "tablet"
5755
+ : "mobile",
5690
5756
  };
5691
5757
  })()
5692
- : { bucket: "unknown", aspectClass: "unknown", densityClass: "unknown", ratio: 0, sizeCategory: "unknown" },
5758
+ : {
5759
+ bucket: "unknown",
5760
+ aspectClass: "unknown",
5761
+ densityClass: "unknown",
5762
+ ratio: 0,
5763
+ sizeCategory: "unknown",
5764
+ },
5693
5765
  // CRITICAL: Do NOT include availability flags (canvas/webgl/audio) in stableCoreVector
5694
5766
  // These flags can change between requests due to timeouts, permissions, or hardware issues
5695
5767
  // and would cause the stableCoreHash to change, breaking visitor identification
@@ -5800,10 +5872,13 @@ async function collectFingerprint(options = {}) {
5800
5872
  arch: archBucket,
5801
5873
  class: getHardwareClass(coresBucket, memoryBucket),
5802
5874
  // 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"
5875
+ touch: deviceSignals$1.touchSupport?.maxTouchPoints
5876
+ ? deviceSignals$1.touchSupport.maxTouchPoints >= 10
5877
+ ? "multi"
5878
+ : deviceSignals$1.touchSupport.maxTouchPoints >= 5
5879
+ ? "standard"
5880
+ : "basic"
5881
+ : "none",
5807
5882
  };
5808
5883
  })(),
5809
5884
  // PRIORITY 8: Enhanced color and display capabilities bucketing
@@ -5829,10 +5904,14 @@ async function collectFingerprint(options = {}) {
5829
5904
  if (!gamut)
5830
5905
  return "unknown";
5831
5906
  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";
5907
+ case "rec2020":
5908
+ return "professional"; // Professional/HDR displays
5909
+ case "p3":
5910
+ return "enhanced"; // Modern displays (Apple, etc.)
5911
+ case "srgb":
5912
+ return "standard"; // Standard displays
5913
+ default:
5914
+ return "unknown";
5836
5915
  }
5837
5916
  };
5838
5917
  // Combined display quality classification
@@ -5856,7 +5935,7 @@ async function collectFingerprint(options = {}) {
5856
5935
  return {
5857
5936
  depth: depthClass,
5858
5937
  gamut: gamutClass,
5859
- class: getDisplayClass(depthClass, gamutClass)
5938
+ class: getDisplayClass(depthClass, gamutClass),
5860
5939
  };
5861
5940
  })(),
5862
5941
  };
@@ -5875,7 +5954,9 @@ async function collectFingerprint(options = {}) {
5875
5954
  // Add a simple checksum for additional uniqueness (deterministic)
5876
5955
  let checksum = 0;
5877
5956
  for (let i = 0; i < entropyString.length; i++) {
5878
- checksum = ((checksum << 5) - checksum + entropyString.charCodeAt(i)) & 0xffffffff;
5957
+ checksum =
5958
+ ((checksum << 5) - checksum + entropyString.charCodeAt(i)) &
5959
+ 0xffffffff;
5879
5960
  }
5880
5961
  coreVector._entropy = Math.abs(checksum).toString(36).substring(0, 8);
5881
5962
  }
@@ -5891,7 +5972,7 @@ async function collectFingerprint(options = {}) {
5891
5972
  // - Vendor flavors (can change)
5892
5973
  // - Exact hardware values (use buckets)
5893
5974
  // Generate ultra-stable hash with additional collision resistance
5894
- const stableCoreHash = hashStableCore(coreVector);
5975
+ const stableCoreHash = hashStableCore(coreVector, opts.debug);
5895
5976
  // CRITICAL: Generate enhanced fingerprint hash combining unstable + stable components
5896
5977
  // This prevents collisions by adding stable entropy to the fingerprint hash
5897
5978
  const enhancedComponentValues = {
@@ -5900,7 +5981,7 @@ async function collectFingerprint(options = {}) {
5900
5981
  // REMOVED: _hourlyEntropy - was causing visitor ID instability (same user = different IDs)
5901
5982
  };
5902
5983
  // Generate collision-resistant fingerprint hash
5903
- const enhancedHash = hashFingerprint(enhancedComponentValues);
5984
+ const enhancedHash = hashFingerprint(enhancedComponentValues, opts.debug);
5904
5985
  // Update finalData with the enhanced hash
5905
5986
  finalData.hash = enhancedHash;
5906
5987
  // Log stable core components for debugging collision issues