@journium/js 1.2.2 → 1.3.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.
package/dist/journium.js CHANGED
@@ -2,6 +2,9 @@
2
2
  (function (exports) {
3
3
  'use strict';
4
4
 
5
+ // @journium/js@1.3.0 is replaced at build time by @rollup/plugin-replace
6
+ const SDK_VERSION = '@journium/js@1.3.0';
7
+
5
8
  /**
6
9
  * uuidv7: A JavaScript implementation of UUID version 7
7
10
  *
@@ -938,6 +941,7 @@
938
941
  Logger.setDebug((_a = this.effectiveOptions.debug) !== null && _a !== void 0 ? _a : false);
939
942
  }
940
943
  buildIdentityProperties(userProperties = {}) {
944
+ var _a, _b;
941
945
  const identity = this.identityManager.getIdentity();
942
946
  const userAgentInfo = this.identityManager.getUserAgentInfo();
943
947
  return {
@@ -948,7 +952,7 @@
948
952
  $current_url: typeof window !== 'undefined' ? window.location.href : '',
949
953
  $pathname: typeof window !== 'undefined' ? window.location.pathname : '',
950
954
  ...userAgentInfo,
951
- $lib_version: '0.1.0', // TODO: Get from package.json
955
+ $sdk_version: (_b = (_a = this.config.options) === null || _a === void 0 ? void 0 : _a._sdkVersion) !== null && _b !== void 0 ? _b : 'unknown',
952
956
  $platform: 'web',
953
957
  ...userProperties,
954
958
  };
@@ -1089,7 +1093,7 @@
1089
1093
  $host: url.host,
1090
1094
  $pathname: url.pathname,
1091
1095
  $search: url.search,
1092
- $title: getPageTitle(),
1096
+ $page_title: getPageTitle(),
1093
1097
  $referrer: getReferrer(),
1094
1098
  ...customProperties,
1095
1099
  };
@@ -1100,28 +1104,31 @@
1100
1104
  * Start automatic autocapture for pageviews
1101
1105
  * @param captureInitialPageview - whether to fire a $pageview immediately on start (default: true).
1102
1106
  * Pass false when restarting after a remote options update to avoid a spurious pageview.
1107
+ * @param patchHistory - whether to monkey-patch pushState/replaceState/popstate (default: true).
1108
+ * Pass false when a framework-native router tracker (e.g. Next.js) owns SPA pageviews.
1103
1109
  */
1104
- startAutoPageviewTracking(captureInitialPageview = true) {
1110
+ startAutoPageviewTracking(captureInitialPageview = true, patchHistory = true) {
1105
1111
  if (captureInitialPageview) {
1106
1112
  this.capturePageview();
1107
1113
  }
1108
- if (typeof window !== 'undefined') {
1109
- // Store original methods for cleanup
1110
- this.originalPushState = window.history.pushState;
1111
- this.originalReplaceState = window.history.replaceState;
1112
- window.history.pushState = (...args) => {
1113
- this.originalPushState.apply(window.history, args);
1114
- setTimeout(() => this.capturePageview(), 0);
1115
- };
1116
- window.history.replaceState = (...args) => {
1117
- this.originalReplaceState.apply(window.history, args);
1118
- setTimeout(() => this.capturePageview(), 0);
1119
- };
1120
- this.popStateHandler = () => {
1121
- setTimeout(() => this.capturePageview(), 0);
1122
- };
1123
- window.addEventListener('popstate', this.popStateHandler);
1114
+ if (!patchHistory || typeof window === 'undefined') {
1115
+ return;
1124
1116
  }
1117
+ // Store original methods for cleanup
1118
+ this.originalPushState = window.history.pushState;
1119
+ this.originalReplaceState = window.history.replaceState;
1120
+ window.history.pushState = (...args) => {
1121
+ this.originalPushState.apply(window.history, args);
1122
+ setTimeout(() => this.capturePageview(), 0);
1123
+ };
1124
+ window.history.replaceState = (...args) => {
1125
+ this.originalReplaceState.apply(window.history, args);
1126
+ setTimeout(() => this.capturePageview(), 0);
1127
+ };
1128
+ this.popStateHandler = () => {
1129
+ setTimeout(() => this.capturePageview(), 0);
1130
+ };
1131
+ window.addEventListener('popstate', this.popStateHandler);
1125
1132
  }
1126
1133
  /**
1127
1134
  * Stop automatic autocapture for pageviews
@@ -1162,6 +1169,8 @@
1162
1169
  ignoreClasses: ['journium-ignore'],
1163
1170
  ignoreElements: ['script', 'style', 'noscript'],
1164
1171
  captureContentText: true,
1172
+ dataAttributePrefixes: ['jrnm-'],
1173
+ dataAttributeNames: ['data-testid', 'data-track'],
1165
1174
  ...options,
1166
1175
  };
1167
1176
  }
@@ -1183,6 +1192,8 @@
1183
1192
  ignoreClasses: ['journium-ignore'],
1184
1193
  ignoreElements: ['script', 'style', 'noscript'],
1185
1194
  captureContentText: true,
1195
+ dataAttributePrefixes: ['jrnm-'],
1196
+ dataAttributeNames: ['data-testid', 'data-track'],
1186
1197
  ...options,
1187
1198
  };
1188
1199
  // Restart if it was active before
@@ -1225,10 +1236,7 @@
1225
1236
  return;
1226
1237
  }
1227
1238
  const properties = this.getElementProperties(target, 'click');
1228
- this.client.track('$autocapture', {
1229
- $event_type: 'click',
1230
- ...properties,
1231
- });
1239
+ this.client.track('$autocapture', properties);
1232
1240
  };
1233
1241
  document.addEventListener('click', clickListener, true);
1234
1242
  this.listeners.set('click', clickListener);
@@ -1240,10 +1248,7 @@
1240
1248
  return;
1241
1249
  }
1242
1250
  const properties = this.getFormProperties(target, 'submit');
1243
- this.client.track('$autocapture', {
1244
- $event_type: 'submit',
1245
- ...properties,
1246
- });
1251
+ this.client.track('$autocapture', properties);
1247
1252
  };
1248
1253
  document.addEventListener('submit', submitListener, true);
1249
1254
  this.listeners.set('submit', submitListener);
@@ -1255,10 +1260,7 @@
1255
1260
  return;
1256
1261
  }
1257
1262
  const properties = this.getInputProperties(target, 'change');
1258
- this.client.track('$autocapture', {
1259
- $event_type: 'change',
1260
- ...properties,
1261
- });
1263
+ this.client.track('$autocapture', properties);
1262
1264
  };
1263
1265
  document.addEventListener('change', changeListener, true);
1264
1266
  this.listeners.set('change', changeListener);
@@ -1311,6 +1313,7 @@
1311
1313
  }
1312
1314
  getElementProperties(element, eventType) {
1313
1315
  const properties = {
1316
+ $event_type: eventType,
1314
1317
  $element_tag: element.tagName.toLowerCase(),
1315
1318
  $element_type: this.getElementType(element),
1316
1319
  };
@@ -1320,6 +1323,7 @@
1320
1323
  }
1321
1324
  if (element.className) {
1322
1325
  properties.$element_classes = Array.from(element.classList);
1326
+ properties.$element_semantic_classes = this.extractSemanticClasses(element.classList);
1323
1327
  }
1324
1328
  // Element attributes
1325
1329
  const relevantAttributes = ['name', 'role', 'aria-label', 'data-testid', 'data-track'];
@@ -1329,6 +1333,33 @@
1329
1333
  properties[`$element_${attr.replace('-', '_')}`] = value;
1330
1334
  }
1331
1335
  });
1336
+ // Configurable data-* attribute capture
1337
+ const prefixes = this.options.dataAttributePrefixes || ['jrnm-'];
1338
+ const exactNames = new Set(this.options.dataAttributeNames || ['data-testid', 'data-track']);
1339
+ const relevantSet = new Set(relevantAttributes);
1340
+ let dataAttrCount = 0;
1341
+ for (let i = 0; i < element.attributes.length && dataAttrCount < 10; i++) {
1342
+ const attr = element.attributes.item(i);
1343
+ if (!attr || !attr.name.startsWith('data-'))
1344
+ continue;
1345
+ if (relevantSet.has(attr.name))
1346
+ continue;
1347
+ const suffix = attr.name.slice(5); // strip 'data-'
1348
+ const matchesPrefix = prefixes.some(p => suffix.startsWith(p));
1349
+ const matchesName = exactNames.has(attr.name);
1350
+ if (matchesPrefix || matchesName) {
1351
+ const propName = `$attr_${attr.name.replace(/-/g, '_')}`;
1352
+ properties[propName] = attr.value;
1353
+ dataAttrCount++;
1354
+ }
1355
+ }
1356
+ // Link href as first-class property
1357
+ if (element.tagName.toLowerCase() === 'a') {
1358
+ const href = element.getAttribute('href');
1359
+ if (href) {
1360
+ properties.$element_href = href;
1361
+ }
1362
+ }
1332
1363
  // Element content
1333
1364
  if (this.options.captureContentText) {
1334
1365
  const text = this.getElementText(element);
@@ -1358,10 +1389,13 @@
1358
1389
  properties.$parent_id = element.parentElement.id;
1359
1390
  }
1360
1391
  }
1361
- // URL information
1392
+ // URL and page context
1362
1393
  properties.$current_url = window.location.href;
1363
1394
  properties.$host = window.location.host;
1364
1395
  properties.$pathname = window.location.pathname;
1396
+ properties.$search = window.location.search;
1397
+ properties.$page_title = document.title;
1398
+ properties.$referrer = document.referrer;
1365
1399
  return properties;
1366
1400
  }
1367
1401
  getFormProperties(form, eventType) {
@@ -1510,6 +1544,38 @@
1510
1544
  ids: ids.reverse()
1511
1545
  };
1512
1546
  }
1547
+ extractSemanticClasses(classList) {
1548
+ const results = new Set();
1549
+ for (let i = 0; i < classList.length; i++) {
1550
+ const cls = classList.item(i);
1551
+ if (!cls)
1552
+ continue;
1553
+ const parts = cls.split('__');
1554
+ if (parts.length >= 3) {
1555
+ // CSS module pattern: Module__hash__name → take last segment
1556
+ const last = parts[parts.length - 1];
1557
+ if (last)
1558
+ results.add(last);
1559
+ }
1560
+ else if (parts.length === 2) {
1561
+ // 2-part __ class (e.g., Module__hash) → drop, no semantic name
1562
+ continue;
1563
+ }
1564
+ else {
1565
+ // Single-part class — keep unless it looks like a hash
1566
+ if (!this.isHashLike(cls)) {
1567
+ results.add(cls);
1568
+ }
1569
+ }
1570
+ }
1571
+ return Array.from(results);
1572
+ }
1573
+ isHashLike(value) {
1574
+ // Hash-like: alphanumeric, 5-10 chars, contains both letters and digits
1575
+ return /^[a-zA-Z0-9]{5,10}$/.test(value)
1576
+ && /[a-zA-Z]/.test(value)
1577
+ && /[0-9]/.test(value);
1578
+ }
1513
1579
  isSafeInputType(type) {
1514
1580
  // Don't capture values for sensitive input types
1515
1581
  const sensitiveTypes = ['password', 'email', 'tel', 'credit-card-number'];
@@ -1536,6 +1602,20 @@
1536
1602
  // This handles cached remote options or local options with autocapture enabled
1537
1603
  this.startAutocaptureIfEnabled(initialEffectiveOptions);
1538
1604
  }
1605
+ resolvePageviewOptions(autoTrackPageviews, frameworkHandlesPageviews) {
1606
+ if (autoTrackPageviews === false || frameworkHandlesPageviews) {
1607
+ return { enabled: false, trackSpaPageviews: false, captureInitialPageview: false };
1608
+ }
1609
+ if (autoTrackPageviews === true || autoTrackPageviews === undefined) {
1610
+ return { enabled: true, trackSpaPageviews: true, captureInitialPageview: true };
1611
+ }
1612
+ // object form implies enabled
1613
+ return {
1614
+ enabled: true,
1615
+ trackSpaPageviews: autoTrackPageviews.trackSpaPageviews !== false,
1616
+ captureInitialPageview: autoTrackPageviews.trackInitialPageview !== false,
1617
+ };
1618
+ }
1539
1619
  resolveAutocaptureOptions(autocapture) {
1540
1620
  if (autocapture === false) {
1541
1621
  return {
@@ -1550,39 +1630,50 @@
1550
1630
  }
1551
1631
  return autocapture;
1552
1632
  }
1633
+ /** Track a custom event with optional properties. */
1553
1634
  track(event, properties) {
1554
1635
  this.client.track(event, properties);
1555
1636
  }
1637
+ /** Associate the current session with a known user identity and optional attributes. */
1556
1638
  identify(distinctId, attributes) {
1557
1639
  this.client.identify(distinctId, attributes);
1558
1640
  }
1641
+ /** Clear the current identity, starting a new anonymous session. */
1559
1642
  reset() {
1560
1643
  this.client.reset();
1561
1644
  }
1645
+ /** Manually capture a $pageview event with optional custom properties. */
1562
1646
  capturePageview(properties) {
1563
1647
  this.pageviewTracker.capturePageview(properties);
1564
1648
  }
1649
+ /**
1650
+ * Manually start autocapture (pageview tracking + DOM event capture).
1651
+ * Under normal usage this is not needed — the SDK starts automatically on init.
1652
+ * Useful only if autocapture was explicitly stopped and needs to be restarted.
1653
+ */
1565
1654
  startAutocapture() {
1566
1655
  // Always check effective options (which may include remote options)
1567
1656
  const effectiveOptions = this.client.getEffectiveOptions();
1568
- // Only enable if effectiveOptions are loaded and autoTrackPageviews is not explicitly false
1569
- const autoTrackPageviews = effectiveOptions && Object.keys(effectiveOptions).length > 0
1570
- ? effectiveOptions.autoTrackPageviews !== false
1571
- : false;
1572
- const autocaptureEnabled = effectiveOptions && Object.keys(effectiveOptions).length > 0
1657
+ // Only start if effectiveOptions are actually loaded (non-empty)
1658
+ const hasOptions = effectiveOptions && Object.keys(effectiveOptions).length > 0;
1659
+ const { enabled: autoTrackPageviews, trackSpaPageviews, captureInitialPageview } = hasOptions
1660
+ ? this.resolvePageviewOptions(effectiveOptions.autoTrackPageviews, effectiveOptions._frameworkHandlesPageviews)
1661
+ : { enabled: false, trackSpaPageviews: false, captureInitialPageview: false };
1662
+ const autocaptureEnabled = hasOptions
1573
1663
  ? effectiveOptions.autocapture !== false
1574
1664
  : false;
1575
1665
  // Update autocapture tracker options if they've changed
1576
1666
  const autocaptureOptions = this.resolveAutocaptureOptions(effectiveOptions.autocapture);
1577
1667
  this.autocaptureTracker.updateOptions(autocaptureOptions);
1578
1668
  if (autoTrackPageviews) {
1579
- this.pageviewTracker.startAutoPageviewTracking();
1669
+ this.pageviewTracker.startAutoPageviewTracking(captureInitialPageview, trackSpaPageviews);
1580
1670
  }
1581
1671
  if (autocaptureEnabled) {
1582
1672
  this.autocaptureTracker.start();
1583
1673
  }
1584
1674
  this.autocaptureStarted = true;
1585
1675
  }
1676
+ /** Stop autocapture — pauses pageview tracking and DOM event capture. */
1586
1677
  stopAutocapture() {
1587
1678
  this.pageviewTracker.stopAutocapture();
1588
1679
  this.autocaptureTracker.stop();
@@ -1602,13 +1693,13 @@
1602
1693
  const hasActualOptions = effectiveOptions && Object.keys(effectiveOptions).length > 0;
1603
1694
  if (hasActualOptions) {
1604
1695
  // Use same logic as manual startAutocapture() but only start automatically
1605
- const autoTrackPageviews = effectiveOptions.autoTrackPageviews !== false;
1696
+ const { enabled: autoTrackPageviews, trackSpaPageviews, captureInitialPageview } = this.resolvePageviewOptions(effectiveOptions.autoTrackPageviews, effectiveOptions._frameworkHandlesPageviews);
1606
1697
  const autocaptureEnabled = effectiveOptions.autocapture !== false;
1607
1698
  // Update autocapture tracker options
1608
1699
  const autocaptureOptions = this.resolveAutocaptureOptions(effectiveOptions.autocapture);
1609
1700
  this.autocaptureTracker.updateOptions(autocaptureOptions);
1610
1701
  if (autoTrackPageviews) {
1611
- this.pageviewTracker.startAutoPageviewTracking();
1702
+ this.pageviewTracker.startAutoPageviewTracking(captureInitialPageview, trackSpaPageviews);
1612
1703
  }
1613
1704
  if (autocaptureEnabled) {
1614
1705
  this.autocaptureTracker.start();
@@ -1633,21 +1724,23 @@
1633
1724
  this.autocaptureTracker.stop();
1634
1725
  this.autocaptureStarted = false;
1635
1726
  }
1636
- const autoTrackPageviews = effectiveOptions.autoTrackPageviews !== false;
1727
+ const { enabled: autoTrackPageviews, trackSpaPageviews, captureInitialPageview } = this.resolvePageviewOptions(effectiveOptions.autoTrackPageviews, effectiveOptions._frameworkHandlesPageviews);
1637
1728
  const autocaptureEnabled = effectiveOptions.autocapture !== false;
1638
1729
  const autocaptureOptions = this.resolveAutocaptureOptions(effectiveOptions.autocapture);
1639
1730
  this.autocaptureTracker.updateOptions(autocaptureOptions);
1640
1731
  if (autoTrackPageviews) {
1641
- this.pageviewTracker.startAutoPageviewTracking(isFirstStart);
1732
+ this.pageviewTracker.startAutoPageviewTracking(isFirstStart && captureInitialPageview, trackSpaPageviews);
1642
1733
  }
1643
1734
  if (autocaptureEnabled) {
1644
1735
  this.autocaptureTracker.start();
1645
1736
  }
1646
1737
  this.autocaptureStarted = autoTrackPageviews || autocaptureEnabled;
1647
1738
  }
1739
+ /** Flush all queued events to the ingestion endpoint immediately. */
1648
1740
  async flush() {
1649
1741
  return this.client.flush();
1650
1742
  }
1743
+ /** Return the currently active options (merged local + remote config). */
1651
1744
  getEffectiveOptions() {
1652
1745
  return this.client.getEffectiveOptions();
1653
1746
  }
@@ -1657,6 +1750,7 @@
1657
1750
  onOptionsChange(callback) {
1658
1751
  return this.client.onOptionsChange(callback);
1659
1752
  }
1753
+ /** Tear down the analytics instance: stop all tracking, flush pending events, and release resources. */
1660
1754
  destroy() {
1661
1755
  this.pageviewTracker.stopAutocapture();
1662
1756
  this.autocaptureTracker.stop();
@@ -1666,7 +1760,16 @@
1666
1760
  this.client.destroy();
1667
1761
  }
1668
1762
  }
1763
+ /** Create and return a new JourniumAnalytics instance for the given config. */
1669
1764
  const init = (config) => {
1765
+ var _a;
1766
+ // Set SDK version if not already set by a framework SDK (React, Next.js, Angular)
1767
+ if (!((_a = config.options) === null || _a === void 0 ? void 0 : _a._sdkVersion)) {
1768
+ config = {
1769
+ ...config,
1770
+ options: { ...config.options, _sdkVersion: SDK_VERSION },
1771
+ };
1772
+ }
1670
1773
  return new JourniumAnalytics(config);
1671
1774
  };
1672
1775