@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/index.umd.js CHANGED
@@ -4,6 +4,9 @@
4
4
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.journium = {}));
5
5
  })(this, (function (exports) { 'use strict';
6
6
 
7
+ // @journium/js@1.3.0 is replaced at build time by @rollup/plugin-replace
8
+ const SDK_VERSION = '@journium/js@1.3.0';
9
+
7
10
  /**
8
11
  * uuidv7: A JavaScript implementation of UUID version 7
9
12
  *
@@ -944,6 +947,7 @@
944
947
  Logger.setDebug((_a = this.effectiveOptions.debug) !== null && _a !== void 0 ? _a : false);
945
948
  }
946
949
  buildIdentityProperties(userProperties = {}) {
950
+ var _a, _b;
947
951
  const identity = this.identityManager.getIdentity();
948
952
  const userAgentInfo = this.identityManager.getUserAgentInfo();
949
953
  return {
@@ -954,7 +958,7 @@
954
958
  $current_url: typeof window !== 'undefined' ? window.location.href : '',
955
959
  $pathname: typeof window !== 'undefined' ? window.location.pathname : '',
956
960
  ...userAgentInfo,
957
- $lib_version: '0.1.0', // TODO: Get from package.json
961
+ $sdk_version: (_b = (_a = this.config.options) === null || _a === void 0 ? void 0 : _a._sdkVersion) !== null && _b !== void 0 ? _b : 'unknown',
958
962
  $platform: 'web',
959
963
  ...userProperties,
960
964
  };
@@ -1095,7 +1099,7 @@
1095
1099
  $host: url.host,
1096
1100
  $pathname: url.pathname,
1097
1101
  $search: url.search,
1098
- $title: getPageTitle(),
1102
+ $page_title: getPageTitle(),
1099
1103
  $referrer: getReferrer(),
1100
1104
  ...customProperties,
1101
1105
  };
@@ -1106,28 +1110,31 @@
1106
1110
  * Start automatic autocapture for pageviews
1107
1111
  * @param captureInitialPageview - whether to fire a $pageview immediately on start (default: true).
1108
1112
  * Pass false when restarting after a remote options update to avoid a spurious pageview.
1113
+ * @param patchHistory - whether to monkey-patch pushState/replaceState/popstate (default: true).
1114
+ * Pass false when a framework-native router tracker (e.g. Next.js) owns SPA pageviews.
1109
1115
  */
1110
- startAutoPageviewTracking(captureInitialPageview = true) {
1116
+ startAutoPageviewTracking(captureInitialPageview = true, patchHistory = true) {
1111
1117
  if (captureInitialPageview) {
1112
1118
  this.capturePageview();
1113
1119
  }
1114
- if (typeof window !== 'undefined') {
1115
- // Store original methods for cleanup
1116
- this.originalPushState = window.history.pushState;
1117
- this.originalReplaceState = window.history.replaceState;
1118
- window.history.pushState = (...args) => {
1119
- this.originalPushState.apply(window.history, args);
1120
- setTimeout(() => this.capturePageview(), 0);
1121
- };
1122
- window.history.replaceState = (...args) => {
1123
- this.originalReplaceState.apply(window.history, args);
1124
- setTimeout(() => this.capturePageview(), 0);
1125
- };
1126
- this.popStateHandler = () => {
1127
- setTimeout(() => this.capturePageview(), 0);
1128
- };
1129
- window.addEventListener('popstate', this.popStateHandler);
1120
+ if (!patchHistory || typeof window === 'undefined') {
1121
+ return;
1130
1122
  }
1123
+ // Store original methods for cleanup
1124
+ this.originalPushState = window.history.pushState;
1125
+ this.originalReplaceState = window.history.replaceState;
1126
+ window.history.pushState = (...args) => {
1127
+ this.originalPushState.apply(window.history, args);
1128
+ setTimeout(() => this.capturePageview(), 0);
1129
+ };
1130
+ window.history.replaceState = (...args) => {
1131
+ this.originalReplaceState.apply(window.history, args);
1132
+ setTimeout(() => this.capturePageview(), 0);
1133
+ };
1134
+ this.popStateHandler = () => {
1135
+ setTimeout(() => this.capturePageview(), 0);
1136
+ };
1137
+ window.addEventListener('popstate', this.popStateHandler);
1131
1138
  }
1132
1139
  /**
1133
1140
  * Stop automatic autocapture for pageviews
@@ -1168,6 +1175,8 @@
1168
1175
  ignoreClasses: ['journium-ignore'],
1169
1176
  ignoreElements: ['script', 'style', 'noscript'],
1170
1177
  captureContentText: true,
1178
+ dataAttributePrefixes: ['jrnm-'],
1179
+ dataAttributeNames: ['data-testid', 'data-track'],
1171
1180
  ...options,
1172
1181
  };
1173
1182
  }
@@ -1189,6 +1198,8 @@
1189
1198
  ignoreClasses: ['journium-ignore'],
1190
1199
  ignoreElements: ['script', 'style', 'noscript'],
1191
1200
  captureContentText: true,
1201
+ dataAttributePrefixes: ['jrnm-'],
1202
+ dataAttributeNames: ['data-testid', 'data-track'],
1192
1203
  ...options,
1193
1204
  };
1194
1205
  // Restart if it was active before
@@ -1231,10 +1242,7 @@
1231
1242
  return;
1232
1243
  }
1233
1244
  const properties = this.getElementProperties(target, 'click');
1234
- this.client.track('$autocapture', {
1235
- $event_type: 'click',
1236
- ...properties,
1237
- });
1245
+ this.client.track('$autocapture', properties);
1238
1246
  };
1239
1247
  document.addEventListener('click', clickListener, true);
1240
1248
  this.listeners.set('click', clickListener);
@@ -1246,10 +1254,7 @@
1246
1254
  return;
1247
1255
  }
1248
1256
  const properties = this.getFormProperties(target, 'submit');
1249
- this.client.track('$autocapture', {
1250
- $event_type: 'submit',
1251
- ...properties,
1252
- });
1257
+ this.client.track('$autocapture', properties);
1253
1258
  };
1254
1259
  document.addEventListener('submit', submitListener, true);
1255
1260
  this.listeners.set('submit', submitListener);
@@ -1261,10 +1266,7 @@
1261
1266
  return;
1262
1267
  }
1263
1268
  const properties = this.getInputProperties(target, 'change');
1264
- this.client.track('$autocapture', {
1265
- $event_type: 'change',
1266
- ...properties,
1267
- });
1269
+ this.client.track('$autocapture', properties);
1268
1270
  };
1269
1271
  document.addEventListener('change', changeListener, true);
1270
1272
  this.listeners.set('change', changeListener);
@@ -1317,6 +1319,7 @@
1317
1319
  }
1318
1320
  getElementProperties(element, eventType) {
1319
1321
  const properties = {
1322
+ $event_type: eventType,
1320
1323
  $element_tag: element.tagName.toLowerCase(),
1321
1324
  $element_type: this.getElementType(element),
1322
1325
  };
@@ -1326,6 +1329,7 @@
1326
1329
  }
1327
1330
  if (element.className) {
1328
1331
  properties.$element_classes = Array.from(element.classList);
1332
+ properties.$element_semantic_classes = this.extractSemanticClasses(element.classList);
1329
1333
  }
1330
1334
  // Element attributes
1331
1335
  const relevantAttributes = ['name', 'role', 'aria-label', 'data-testid', 'data-track'];
@@ -1335,6 +1339,33 @@
1335
1339
  properties[`$element_${attr.replace('-', '_')}`] = value;
1336
1340
  }
1337
1341
  });
1342
+ // Configurable data-* attribute capture
1343
+ const prefixes = this.options.dataAttributePrefixes || ['jrnm-'];
1344
+ const exactNames = new Set(this.options.dataAttributeNames || ['data-testid', 'data-track']);
1345
+ const relevantSet = new Set(relevantAttributes);
1346
+ let dataAttrCount = 0;
1347
+ for (let i = 0; i < element.attributes.length && dataAttrCount < 10; i++) {
1348
+ const attr = element.attributes.item(i);
1349
+ if (!attr || !attr.name.startsWith('data-'))
1350
+ continue;
1351
+ if (relevantSet.has(attr.name))
1352
+ continue;
1353
+ const suffix = attr.name.slice(5); // strip 'data-'
1354
+ const matchesPrefix = prefixes.some(p => suffix.startsWith(p));
1355
+ const matchesName = exactNames.has(attr.name);
1356
+ if (matchesPrefix || matchesName) {
1357
+ const propName = `$attr_${attr.name.replace(/-/g, '_')}`;
1358
+ properties[propName] = attr.value;
1359
+ dataAttrCount++;
1360
+ }
1361
+ }
1362
+ // Link href as first-class property
1363
+ if (element.tagName.toLowerCase() === 'a') {
1364
+ const href = element.getAttribute('href');
1365
+ if (href) {
1366
+ properties.$element_href = href;
1367
+ }
1368
+ }
1338
1369
  // Element content
1339
1370
  if (this.options.captureContentText) {
1340
1371
  const text = this.getElementText(element);
@@ -1364,10 +1395,13 @@
1364
1395
  properties.$parent_id = element.parentElement.id;
1365
1396
  }
1366
1397
  }
1367
- // URL information
1398
+ // URL and page context
1368
1399
  properties.$current_url = window.location.href;
1369
1400
  properties.$host = window.location.host;
1370
1401
  properties.$pathname = window.location.pathname;
1402
+ properties.$search = window.location.search;
1403
+ properties.$page_title = document.title;
1404
+ properties.$referrer = document.referrer;
1371
1405
  return properties;
1372
1406
  }
1373
1407
  getFormProperties(form, eventType) {
@@ -1516,6 +1550,38 @@
1516
1550
  ids: ids.reverse()
1517
1551
  };
1518
1552
  }
1553
+ extractSemanticClasses(classList) {
1554
+ const results = new Set();
1555
+ for (let i = 0; i < classList.length; i++) {
1556
+ const cls = classList.item(i);
1557
+ if (!cls)
1558
+ continue;
1559
+ const parts = cls.split('__');
1560
+ if (parts.length >= 3) {
1561
+ // CSS module pattern: Module__hash__name → take last segment
1562
+ const last = parts[parts.length - 1];
1563
+ if (last)
1564
+ results.add(last);
1565
+ }
1566
+ else if (parts.length === 2) {
1567
+ // 2-part __ class (e.g., Module__hash) → drop, no semantic name
1568
+ continue;
1569
+ }
1570
+ else {
1571
+ // Single-part class — keep unless it looks like a hash
1572
+ if (!this.isHashLike(cls)) {
1573
+ results.add(cls);
1574
+ }
1575
+ }
1576
+ }
1577
+ return Array.from(results);
1578
+ }
1579
+ isHashLike(value) {
1580
+ // Hash-like: alphanumeric, 5-10 chars, contains both letters and digits
1581
+ return /^[a-zA-Z0-9]{5,10}$/.test(value)
1582
+ && /[a-zA-Z]/.test(value)
1583
+ && /[0-9]/.test(value);
1584
+ }
1519
1585
  isSafeInputType(type) {
1520
1586
  // Don't capture values for sensitive input types
1521
1587
  const sensitiveTypes = ['password', 'email', 'tel', 'credit-card-number'];
@@ -1542,6 +1608,20 @@
1542
1608
  // This handles cached remote options or local options with autocapture enabled
1543
1609
  this.startAutocaptureIfEnabled(initialEffectiveOptions);
1544
1610
  }
1611
+ resolvePageviewOptions(autoTrackPageviews, frameworkHandlesPageviews) {
1612
+ if (autoTrackPageviews === false || frameworkHandlesPageviews) {
1613
+ return { enabled: false, trackSpaPageviews: false, captureInitialPageview: false };
1614
+ }
1615
+ if (autoTrackPageviews === true || autoTrackPageviews === undefined) {
1616
+ return { enabled: true, trackSpaPageviews: true, captureInitialPageview: true };
1617
+ }
1618
+ // object form implies enabled
1619
+ return {
1620
+ enabled: true,
1621
+ trackSpaPageviews: autoTrackPageviews.trackSpaPageviews !== false,
1622
+ captureInitialPageview: autoTrackPageviews.trackInitialPageview !== false,
1623
+ };
1624
+ }
1545
1625
  resolveAutocaptureOptions(autocapture) {
1546
1626
  if (autocapture === false) {
1547
1627
  return {
@@ -1556,39 +1636,50 @@
1556
1636
  }
1557
1637
  return autocapture;
1558
1638
  }
1639
+ /** Track a custom event with optional properties. */
1559
1640
  track(event, properties) {
1560
1641
  this.client.track(event, properties);
1561
1642
  }
1643
+ /** Associate the current session with a known user identity and optional attributes. */
1562
1644
  identify(distinctId, attributes) {
1563
1645
  this.client.identify(distinctId, attributes);
1564
1646
  }
1647
+ /** Clear the current identity, starting a new anonymous session. */
1565
1648
  reset() {
1566
1649
  this.client.reset();
1567
1650
  }
1651
+ /** Manually capture a $pageview event with optional custom properties. */
1568
1652
  capturePageview(properties) {
1569
1653
  this.pageviewTracker.capturePageview(properties);
1570
1654
  }
1655
+ /**
1656
+ * Manually start autocapture (pageview tracking + DOM event capture).
1657
+ * Under normal usage this is not needed — the SDK starts automatically on init.
1658
+ * Useful only if autocapture was explicitly stopped and needs to be restarted.
1659
+ */
1571
1660
  startAutocapture() {
1572
1661
  // Always check effective options (which may include remote options)
1573
1662
  const effectiveOptions = this.client.getEffectiveOptions();
1574
- // Only enable if effectiveOptions are loaded and autoTrackPageviews is not explicitly false
1575
- const autoTrackPageviews = effectiveOptions && Object.keys(effectiveOptions).length > 0
1576
- ? effectiveOptions.autoTrackPageviews !== false
1577
- : false;
1578
- const autocaptureEnabled = effectiveOptions && Object.keys(effectiveOptions).length > 0
1663
+ // Only start if effectiveOptions are actually loaded (non-empty)
1664
+ const hasOptions = effectiveOptions && Object.keys(effectiveOptions).length > 0;
1665
+ const { enabled: autoTrackPageviews, trackSpaPageviews, captureInitialPageview } = hasOptions
1666
+ ? this.resolvePageviewOptions(effectiveOptions.autoTrackPageviews, effectiveOptions._frameworkHandlesPageviews)
1667
+ : { enabled: false, trackSpaPageviews: false, captureInitialPageview: false };
1668
+ const autocaptureEnabled = hasOptions
1579
1669
  ? effectiveOptions.autocapture !== false
1580
1670
  : false;
1581
1671
  // Update autocapture tracker options if they've changed
1582
1672
  const autocaptureOptions = this.resolveAutocaptureOptions(effectiveOptions.autocapture);
1583
1673
  this.autocaptureTracker.updateOptions(autocaptureOptions);
1584
1674
  if (autoTrackPageviews) {
1585
- this.pageviewTracker.startAutoPageviewTracking();
1675
+ this.pageviewTracker.startAutoPageviewTracking(captureInitialPageview, trackSpaPageviews);
1586
1676
  }
1587
1677
  if (autocaptureEnabled) {
1588
1678
  this.autocaptureTracker.start();
1589
1679
  }
1590
1680
  this.autocaptureStarted = true;
1591
1681
  }
1682
+ /** Stop autocapture — pauses pageview tracking and DOM event capture. */
1592
1683
  stopAutocapture() {
1593
1684
  this.pageviewTracker.stopAutocapture();
1594
1685
  this.autocaptureTracker.stop();
@@ -1608,13 +1699,13 @@
1608
1699
  const hasActualOptions = effectiveOptions && Object.keys(effectiveOptions).length > 0;
1609
1700
  if (hasActualOptions) {
1610
1701
  // Use same logic as manual startAutocapture() but only start automatically
1611
- const autoTrackPageviews = effectiveOptions.autoTrackPageviews !== false;
1702
+ const { enabled: autoTrackPageviews, trackSpaPageviews, captureInitialPageview } = this.resolvePageviewOptions(effectiveOptions.autoTrackPageviews, effectiveOptions._frameworkHandlesPageviews);
1612
1703
  const autocaptureEnabled = effectiveOptions.autocapture !== false;
1613
1704
  // Update autocapture tracker options
1614
1705
  const autocaptureOptions = this.resolveAutocaptureOptions(effectiveOptions.autocapture);
1615
1706
  this.autocaptureTracker.updateOptions(autocaptureOptions);
1616
1707
  if (autoTrackPageviews) {
1617
- this.pageviewTracker.startAutoPageviewTracking();
1708
+ this.pageviewTracker.startAutoPageviewTracking(captureInitialPageview, trackSpaPageviews);
1618
1709
  }
1619
1710
  if (autocaptureEnabled) {
1620
1711
  this.autocaptureTracker.start();
@@ -1639,21 +1730,23 @@
1639
1730
  this.autocaptureTracker.stop();
1640
1731
  this.autocaptureStarted = false;
1641
1732
  }
1642
- const autoTrackPageviews = effectiveOptions.autoTrackPageviews !== false;
1733
+ const { enabled: autoTrackPageviews, trackSpaPageviews, captureInitialPageview } = this.resolvePageviewOptions(effectiveOptions.autoTrackPageviews, effectiveOptions._frameworkHandlesPageviews);
1643
1734
  const autocaptureEnabled = effectiveOptions.autocapture !== false;
1644
1735
  const autocaptureOptions = this.resolveAutocaptureOptions(effectiveOptions.autocapture);
1645
1736
  this.autocaptureTracker.updateOptions(autocaptureOptions);
1646
1737
  if (autoTrackPageviews) {
1647
- this.pageviewTracker.startAutoPageviewTracking(isFirstStart);
1738
+ this.pageviewTracker.startAutoPageviewTracking(isFirstStart && captureInitialPageview, trackSpaPageviews);
1648
1739
  }
1649
1740
  if (autocaptureEnabled) {
1650
1741
  this.autocaptureTracker.start();
1651
1742
  }
1652
1743
  this.autocaptureStarted = autoTrackPageviews || autocaptureEnabled;
1653
1744
  }
1745
+ /** Flush all queued events to the ingestion endpoint immediately. */
1654
1746
  async flush() {
1655
1747
  return this.client.flush();
1656
1748
  }
1749
+ /** Return the currently active options (merged local + remote config). */
1657
1750
  getEffectiveOptions() {
1658
1751
  return this.client.getEffectiveOptions();
1659
1752
  }
@@ -1663,6 +1756,7 @@
1663
1756
  onOptionsChange(callback) {
1664
1757
  return this.client.onOptionsChange(callback);
1665
1758
  }
1759
+ /** Tear down the analytics instance: stop all tracking, flush pending events, and release resources. */
1666
1760
  destroy() {
1667
1761
  this.pageviewTracker.stopAutocapture();
1668
1762
  this.autocaptureTracker.stop();
@@ -1672,7 +1766,16 @@
1672
1766
  this.client.destroy();
1673
1767
  }
1674
1768
  }
1769
+ /** Create and return a new JourniumAnalytics instance for the given config. */
1675
1770
  const init = (config) => {
1771
+ var _a;
1772
+ // Set SDK version if not already set by a framework SDK (React, Next.js, Angular)
1773
+ if (!((_a = config.options) === null || _a === void 0 ? void 0 : _a._sdkVersion)) {
1774
+ config = {
1775
+ ...config,
1776
+ options: { ...config.options, _sdkVersion: SDK_VERSION },
1777
+ };
1778
+ }
1676
1779
  return new JourniumAnalytics(config);
1677
1780
  };
1678
1781