@datalyr/web 1.0.6 → 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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @datalyr/web v1.0.6
2
+ * @datalyr/web v1.1.0
3
3
  * Datalyr Web SDK - Modern attribution tracking for web applications
4
4
  * (c) 2025 Datalyr Inc.
5
5
  * Released under the MIT License
@@ -161,7 +161,16 @@ class CookieStorage {
161
161
  const value = `; ${document.cookie}`;
162
162
  const parts = value.split(`; ${name}=`);
163
163
  if (parts.length === 2) {
164
- return ((_a = parts.pop()) === null || _a === void 0 ? void 0 : _a.split(';').shift()) || null;
164
+ const rawValue = ((_a = parts.pop()) === null || _a === void 0 ? void 0 : _a.split(';').shift()) || null;
165
+ if (rawValue) {
166
+ try {
167
+ return decodeURIComponent(rawValue);
168
+ }
169
+ catch (_b) {
170
+ // Return raw value if decoding fails (backwards compatibility)
171
+ return rawValue;
172
+ }
173
+ }
165
174
  }
166
175
  return null;
167
176
  }
@@ -395,6 +404,33 @@ function isGlobalPrivacyControlEnabled() {
395
404
  return navigator.globalPrivacyControl === true ||
396
405
  window.globalPrivacyControl === true;
397
406
  }
407
+ /**
408
+ * Get root domain for cross-subdomain tracking
409
+ */
410
+ function getRootDomain() {
411
+ const hostname = window.location.hostname;
412
+ // Handle localhost and IP addresses
413
+ if (hostname === 'localhost' ||
414
+ hostname.match(/^[0-9]{1,3}\./) || // IPv4
415
+ hostname.match(/^\[?[0-9a-fA-F:]+\]?$/)) { // IPv6
416
+ return hostname;
417
+ }
418
+ // Get root domain (last two parts: example.com)
419
+ const parts = hostname.split('.');
420
+ if (parts.length >= 2) {
421
+ // Handle .co.uk, .com.au, etc
422
+ const tld = parts[parts.length - 1];
423
+ const sld = parts[parts.length - 2];
424
+ // Common two-part TLDs
425
+ const twoPartTlds = ['co.uk', 'com.au', 'co.nz', 'co.jp', 'co.in', 'co.za'];
426
+ const lastTwo = `${sld}.${tld}`;
427
+ if (twoPartTlds.includes(lastTwo) && parts.length >= 3) {
428
+ return '.' + parts.slice(-3).join('.');
429
+ }
430
+ return '.' + parts.slice(-2).join('.');
431
+ }
432
+ return hostname;
433
+ }
398
434
  /**
399
435
  * Get referrer data
400
436
  */
@@ -458,13 +494,61 @@ class IdentityManager {
458
494
  * Get or create anonymous ID (device/browser identifier)
459
495
  */
460
496
  getOrCreateAnonymousId() {
461
- let anonymousId = storage.get('dl_anonymous_id');
462
- if (!anonymousId) {
463
- anonymousId = `anon_${generateUUID()}`;
497
+ // 1. Check root domain cookie first (works across subdomains)
498
+ let anonymousId = cookies.get('__dl_visitor_id');
499
+ if (anonymousId) {
500
+ // Found in cookie - sync to localStorage
464
501
  storage.set('dl_anonymous_id', anonymousId);
465
- }
502
+ return anonymousId;
503
+ }
504
+ // 2. Check localStorage (fallback for cookie issues)
505
+ anonymousId = storage.get('dl_anonymous_id');
506
+ if (anonymousId) {
507
+ // Found in localStorage - set root domain cookie
508
+ this.setRootDomainCookie('__dl_visitor_id', anonymousId);
509
+ return anonymousId;
510
+ }
511
+ // 3. Generate new ID
512
+ anonymousId = `anon_${generateUUID()}`;
513
+ // 4. Store in both cookie (primary) and localStorage (backup)
514
+ this.setRootDomainCookie('__dl_visitor_id', anonymousId);
515
+ storage.set('dl_anonymous_id', anonymousId);
466
516
  return anonymousId;
467
517
  }
518
+ /**
519
+ * Set a root domain cookie for cross-subdomain tracking
520
+ */
521
+ setRootDomainCookie(name, value) {
522
+ try {
523
+ const rootDomain = getRootDomain();
524
+ const secure = location.protocol === 'https:' ? '; Secure' : '';
525
+ const encodedValue = encodeURIComponent(value);
526
+ // Set cookie with root domain, 1 year expiry
527
+ document.cookie = `${name}=${encodedValue}; domain=${rootDomain}; path=/; max-age=31536000; SameSite=Lax${secure}`;
528
+ // Verify cookie was set successfully (cookies.get already decodes)
529
+ const verifyValue = cookies.get(name);
530
+ if (verifyValue === value) {
531
+ console.log(`[Datalyr] Set root domain cookie: ${name} on domain: ${rootDomain}`);
532
+ }
533
+ else {
534
+ // Fallback: try without domain (current subdomain only)
535
+ document.cookie = `${name}=${encodedValue}; path=/; max-age=31536000; SameSite=Lax${secure}`;
536
+ console.log(`[Datalyr] Set cookie without domain (fallback): ${name}`);
537
+ }
538
+ }
539
+ catch (e) {
540
+ console.error('[Datalyr] Error setting root domain cookie:', e);
541
+ // Still try to set without domain as fallback
542
+ try {
543
+ const secure = location.protocol === 'https:' ? '; Secure' : '';
544
+ const encodedValue = encodeURIComponent(value);
545
+ document.cookie = `${name}=${encodedValue}; path=/; max-age=31536000; SameSite=Lax${secure}`;
546
+ }
547
+ catch (fallbackError) {
548
+ console.error('[Datalyr] Failed to set cookie even without domain:', fallbackError);
549
+ }
550
+ }
551
+ }
468
552
  /**
469
553
  * Get stored user ID from previous session
470
554
  */
@@ -558,6 +642,8 @@ class IdentityManager {
558
642
  // Generate new anonymous ID for privacy
559
643
  this.anonymousId = `anon_${generateUUID()}`;
560
644
  storage.set('dl_anonymous_id', this.anonymousId);
645
+ // Update root domain cookie with new ID
646
+ this.setRootDomainCookie('__dl_visitor_id', this.anonymousId);
561
647
  }
562
648
  /**
563
649
  * Get all identity fields for event payload
@@ -1537,24 +1623,40 @@ class FingerprintCollector {
1537
1623
  }
1538
1624
  /**
1539
1625
  * Collect minimal fingerprint data (privacy-friendly)
1626
+ * Matches browser tag minimal fingerprinting approach
1540
1627
  */
1541
1628
  collectMinimal() {
1542
- return {
1629
+ const fp = {
1543
1630
  timezone: this.getTimezone(),
1544
1631
  language: navigator.language || null,
1545
- platform: navigator.platform || null,
1546
- canvasEnabled: false,
1547
- localStorageAvailable: this.testStorage('localStorage'),
1548
- sessionStorageAvailable: this.testStorage('sessionStorage')
1632
+ screen_bucket: this.getScreenBucket(),
1633
+ dnt: (navigator.doNotTrack === '1' || window.globalPrivacyControl === true) || null,
1634
+ userAgent: navigator.userAgent || null
1549
1635
  };
1636
+ // User-Agent Client Hints (modern browsers)
1637
+ if ('userAgentData' in navigator) {
1638
+ const uaData = navigator.userAgentData;
1639
+ fp.userAgentData = {
1640
+ brands: uaData.brands || [],
1641
+ mobile: uaData.mobile || false,
1642
+ platform: uaData.platform || null
1643
+ };
1644
+ }
1645
+ return fp;
1550
1646
  }
1551
1647
  /**
1552
1648
  * Collect standard fingerprint data
1649
+ * PRIVACY: Minimal data collection matching browser tag approach
1650
+ * Only collects data necessary for basic analytics and attribution
1553
1651
  */
1554
1652
  collectStandard() {
1555
1653
  const fingerprint = {};
1556
1654
  try {
1557
- // Basic browser data
1655
+ // PRIVACY: Only collect minimal data needed for attribution
1656
+ fingerprint.timezone = this.getTimezone();
1657
+ fingerprint.language = navigator.language || null;
1658
+ fingerprint.screen_bucket = this.getScreenBucket();
1659
+ fingerprint.dnt = (navigator.doNotTrack === '1' || window.globalPrivacyControl === true) || null;
1558
1660
  fingerprint.userAgent = navigator.userAgent || null;
1559
1661
  // User-Agent Client Hints (modern browsers)
1560
1662
  if ('userAgentData' in navigator) {
@@ -1565,125 +1667,14 @@ class FingerprintCollector {
1565
1667
  platform: uaData.platform || null
1566
1668
  };
1567
1669
  }
1568
- // Language settings
1569
- fingerprint.language = navigator.language || null;
1570
- fingerprint.languages = navigator.languages ?
1571
- navigator.languages.slice(0, 2) : null; // Limit to 2 for privacy
1572
- // Platform and browser features
1573
- fingerprint.platform = navigator.platform || null;
1574
- fingerprint.cookieEnabled = navigator.cookieEnabled || null;
1575
- fingerprint.doNotTrack = navigator.doNotTrack || null;
1576
- // Hardware (coarsened for privacy)
1577
- fingerprint.hardwareConcurrency = this.coarsenHardwareConcurrency();
1578
- fingerprint.deviceMemory = this.coarsenDeviceMemory();
1579
- fingerprint.maxTouchPoints = navigator.maxTouchPoints > 0 ? 'touch' : 'no-touch';
1580
- // Screen (coarsened)
1581
- fingerprint.screenResolution = this.getScreenResolution();
1582
- fingerprint.colorDepth = screen.colorDepth || null;
1583
- fingerprint.pixelRatio = this.coarsenPixelRatio();
1584
- // Timezone
1585
- fingerprint.timezone = this.getTimezone();
1586
- fingerprint.timezoneOffset = this.coarsenTimezoneOffset();
1587
- // Canvas fingerprinting disabled for privacy
1588
- fingerprint.canvasEnabled = false;
1589
- // Plugins count only (not details for privacy)
1590
- fingerprint.pluginsCount = navigator.plugins ? navigator.plugins.length : null;
1591
- // Storage availability
1592
- fingerprint.localStorageAvailable = this.testStorage('localStorage');
1593
- fingerprint.sessionStorageAvailable = this.testStorage('sessionStorage');
1594
- fingerprint.indexedDBAvailable = this.testIndexedDB();
1595
- // Add cached heavy fingerprint data if available
1596
- if (this.heavyFingerprintDone) {
1597
- Object.assign(fingerprint, this.fingerprintCache);
1598
- }
1599
1670
  }
1600
1671
  catch (e) {
1601
1672
  console.warn('[Datalyr] Error collecting fingerprint:', e);
1602
1673
  }
1603
1674
  return fingerprint;
1604
1675
  }
1605
- /**
1606
- * Collect heavy fingerprint data (WebGL, Audio)
1607
- * Called lazily on first event to improve page load performance
1608
- */
1609
- collectHeavyFingerprint() {
1610
- return __awaiter(this, void 0, void 0, function* () {
1611
- if (this.heavyFingerprintDone || this.privacyMode === 'strict') {
1612
- return;
1613
- }
1614
- try {
1615
- // WebGL fingerprinting
1616
- const webglData = this.getWebGLFingerprint();
1617
- if (webglData) {
1618
- this.fingerprintCache.webglVendor = webglData.vendor;
1619
- this.fingerprintCache.webglRenderer = webglData.renderer;
1620
- }
1621
- // Audio fingerprinting
1622
- const audioData = yield this.getAudioFingerprint();
1623
- if (audioData) {
1624
- this.fingerprintCache.audioSampleRate = audioData.sampleRate;
1625
- this.fingerprintCache.audioState = audioData.state;
1626
- this.fingerprintCache.audioMaxChannels = audioData.maxChannels;
1627
- }
1628
- this.heavyFingerprintDone = true;
1629
- }
1630
- catch (e) {
1631
- console.warn('[Datalyr] Heavy fingerprinting failed:', e);
1632
- }
1633
- });
1634
- }
1635
- /**
1636
- * Get WebGL fingerprint
1637
- */
1638
- getWebGLFingerprint() {
1639
- try {
1640
- const canvas = document.createElement('canvas');
1641
- const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
1642
- if (!gl)
1643
- return null;
1644
- const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
1645
- if (!debugInfo) {
1646
- return {
1647
- vendor: gl.getParameter(gl.VENDOR) || 'unknown',
1648
- renderer: gl.getParameter(gl.RENDERER) || 'unknown'
1649
- };
1650
- }
1651
- return {
1652
- vendor: gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) || 'unknown',
1653
- renderer: gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) || 'unknown'
1654
- };
1655
- }
1656
- catch (_a) {
1657
- return null;
1658
- }
1659
- }
1660
- /**
1661
- * Get audio fingerprint
1662
- */
1663
- getAudioFingerprint() {
1664
- return __awaiter(this, void 0, void 0, function* () {
1665
- var _a;
1666
- try {
1667
- const AudioContext = window.AudioContext || window.webkitAudioContext;
1668
- if (!AudioContext)
1669
- return null;
1670
- const audioCtx = new AudioContext();
1671
- const result = {
1672
- sampleRate: audioCtx.sampleRate || null,
1673
- state: audioCtx.state || null,
1674
- maxChannels: ((_a = audioCtx.destination) === null || _a === void 0 ? void 0 : _a.maxChannelCount) || null
1675
- };
1676
- // Close context to free resources
1677
- if (audioCtx.close) {
1678
- yield audioCtx.close();
1679
- }
1680
- return result;
1681
- }
1682
- catch (_b) {
1683
- return null;
1684
- }
1685
- });
1686
- }
1676
+ // PRIVACY: Heavy fingerprinting (WebGL, Audio) removed
1677
+ // Minimal fingerprinting only for privacy compliance
1687
1678
  /**
1688
1679
  * Get timezone
1689
1680
  */
@@ -1696,10 +1687,13 @@ class FingerprintCollector {
1696
1687
  }
1697
1688
  }
1698
1689
  /**
1699
- * Get screen resolution (coarsened)
1690
+ * Get coarse screen bucket for basic device classification
1691
+ * Rounds to nearest 100px for privacy
1700
1692
  */
1701
- getScreenResolution() {
1693
+ getScreenBucket() {
1702
1694
  try {
1695
+ if (!window.screen)
1696
+ return null;
1703
1697
  const width = Math.round(screen.width / 100) * 100;
1704
1698
  const height = Math.round(screen.height / 100) * 100;
1705
1699
  return `${width}x${height}`;
@@ -1708,96 +1702,8 @@ class FingerprintCollector {
1708
1702
  return null;
1709
1703
  }
1710
1704
  }
1711
- /**
1712
- * Coarsen hardware concurrency for privacy
1713
- */
1714
- coarsenHardwareConcurrency() {
1715
- try {
1716
- const cores = navigator.hardwareConcurrency;
1717
- if (!cores)
1718
- return null;
1719
- return cores > 8 ? '8+' : cores;
1720
- }
1721
- catch (_a) {
1722
- return null;
1723
- }
1724
- }
1725
- /**
1726
- * Coarsen device memory for privacy
1727
- */
1728
- coarsenDeviceMemory() {
1729
- try {
1730
- const memory = navigator.deviceMemory;
1731
- if (!memory)
1732
- return null;
1733
- return memory > 4 ? '4+' : memory;
1734
- }
1735
- catch (_a) {
1736
- return null;
1737
- }
1738
- }
1739
- /**
1740
- * Coarsen pixel ratio for privacy
1741
- */
1742
- coarsenPixelRatio() {
1743
- try {
1744
- const ratio = window.devicePixelRatio;
1745
- if (!ratio)
1746
- return null;
1747
- // Round to common values
1748
- if (ratio <= 1)
1749
- return '1';
1750
- if (ratio <= 1.5)
1751
- return '1.5';
1752
- if (ratio <= 2)
1753
- return '2';
1754
- if (ratio <= 3)
1755
- return '3';
1756
- return '3+';
1757
- }
1758
- catch (_a) {
1759
- return null;
1760
- }
1761
- }
1762
- /**
1763
- * Coarsen timezone offset for privacy
1764
- */
1765
- coarsenTimezoneOffset() {
1766
- try {
1767
- const offset = new Date().getTimezoneOffset();
1768
- // Round to nearest 30 minutes
1769
- return Math.round(offset / 30) * 30;
1770
- }
1771
- catch (_a) {
1772
- return null;
1773
- }
1774
- }
1775
- /**
1776
- * Test if storage is available
1777
- */
1778
- testStorage(type) {
1779
- try {
1780
- const storage = window[type];
1781
- const testKey = '__dl_test__';
1782
- storage.setItem(testKey, '1');
1783
- storage.removeItem(testKey);
1784
- return true;
1785
- }
1786
- catch (_a) {
1787
- return false;
1788
- }
1789
- }
1790
- /**
1791
- * Test if IndexedDB is available
1792
- */
1793
- testIndexedDB() {
1794
- try {
1795
- return !!window.indexedDB;
1796
- }
1797
- catch (_a) {
1798
- return false;
1799
- }
1800
- }
1705
+ // PRIVACY: Unused helper methods removed (coarsen functions, storage tests)
1706
+ // Minimal fingerprinting only
1801
1707
  /**
1802
1708
  * Generate fingerprint hash
1803
1709
  */
@@ -2437,13 +2343,7 @@ class Datalyr {
2437
2343
  if (!this.shouldTrack())
2438
2344
  return;
2439
2345
  try {
2440
- // Collect heavy fingerprint on first event (lazy loading)
2441
- if (!this.heavyFingerprintCollected && this.config.enableFingerprinting) {
2442
- this.heavyFingerprintCollected = true;
2443
- this.fingerprint.collectHeavyFingerprint().catch(err => {
2444
- this.log('Heavy fingerprint collection failed:', err);
2445
- });
2446
- }
2346
+ // PRIVACY: Heavy fingerprinting removed - using minimal fingerprinting only
2447
2347
  // Update session activity
2448
2348
  this.session.updateActivity(eventName);
2449
2349
  // Create event payload