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