@jitsu/js 1.10.2 → 1.10.4

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.
@@ -651,7 +651,7 @@ const defaultConfig = {
651
651
  dontSend: false,
652
652
  disableUserIds: false,
653
653
  ipPolicy: "keep",
654
- consentCategories: undefined,
654
+ consentCategories: {},
655
655
  },
656
656
  };
657
657
  // mergeConfig merges newConfig into currentConfig also making sure that all undefined values are replaced with defaultConfig values
@@ -660,11 +660,14 @@ const mergeConfig = (current, newConfig) => {
660
660
  const value = newConfig[key];
661
661
  if (key === "privacy") {
662
662
  if (typeof value === "object") {
663
- current.privacy = Object.assign(Object.assign(Object.assign({}, defaultConfig.privacy), current.privacy), value);
663
+ current.privacy = Object.assign(Object.assign(Object.assign(Object.assign({}, defaultConfig.privacy), { consentCategories: Object.assign({}, defaultConfig.privacy.consentCategories) }), current.privacy), value);
664
664
  }
665
- else if (newConfig.hasOwnProperty("privacy") && typeof value === "undefined") {
666
- // explicitly set to undefined - reset to default
667
- current.privacy = Object.assign({}, defaultConfig.privacy);
665
+ else if (typeof value === "undefined") {
666
+ if (newConfig.hasOwnProperty("privacy") || !current.hasOwnProperty("privacy")) {
667
+ // explicitly set to undefined - reset to default
668
+ // or was not set at all - set to default
669
+ current.privacy = Object.assign(Object.assign({}, defaultConfig.privacy), { consentCategories: Object.assign({}, defaultConfig.privacy.consentCategories) });
670
+ }
668
671
  }
669
672
  }
670
673
  else if (typeof value === "undefined") {
@@ -739,6 +742,19 @@ function getClientIds(runtime, customCookieCapture) {
739
742
  }, {});
740
743
  return Object.assign(Object.assign({}, clientIds), getGa4Ids(runtime));
741
744
  }
745
+ function parseGa4SessionId(cookieValue) {
746
+ if (typeof cookieValue !== "string") {
747
+ return undefined;
748
+ }
749
+ if (cookieValue.startsWith("GA1") || cookieValue.startsWith("GS1")) {
750
+ return cookieValue.split(".")[2];
751
+ }
752
+ else {
753
+ // parse new GA4 cookie format, e.g.: GS2.1.s1747323152$o28$g0$t1747323152$j60$l0$h69286059
754
+ const match = cookieValue.match(/^GS\d+\.\d+\.(?:[\w_-]+[$])*s(\d+)(?:$|[$])/);
755
+ return match ? match[1] : undefined;
756
+ }
757
+ }
742
758
  function getGa4Ids(runtime) {
743
759
  var _a;
744
760
  const allCookies = runtime.getCookies();
@@ -747,14 +763,11 @@ function getGa4Ids(runtime) {
747
763
  const sessionIds = gaSessionCookies.length > 0
748
764
  ? Object.fromEntries(gaSessionCookies
749
765
  .map(([key, value]) => {
750
- if (typeof value !== "string") {
751
- return null;
752
- }
753
- const parts = value.split(".");
754
- if (parts.length < 3) {
766
+ const sessionId = parseGa4SessionId(value);
767
+ if (!sessionId) {
755
768
  return null;
756
769
  }
757
- return [key.substring("_ga_".length), parts[2]];
770
+ return [key.substring("_ga_".length), sessionId];
758
771
  })
759
772
  .filter(v => v !== null))
760
773
  : undefined;
@@ -978,6 +991,15 @@ const emptyRuntime = (config) => ({
978
991
  return undefined;
979
992
  },
980
993
  });
994
+ function deepCopy(o) {
995
+ if (typeof o !== "object") {
996
+ return o;
997
+ }
998
+ if (!o) {
999
+ return o;
1000
+ }
1001
+ return JSON.parse(JSON.stringify(o));
1002
+ }
981
1003
  function deepMerge(target, source) {
982
1004
  if (typeof source !== "object" || source === null) {
983
1005
  return source;
@@ -1014,17 +1036,46 @@ function urlPath(url) {
1014
1036
  const pathMatch = matches && matches[3] ? matches[3].split("?")[0].replace(hashRegex, "") : "";
1015
1037
  return "/" + pathMatch;
1016
1038
  }
1039
+ function canonicalUrl() {
1040
+ if (!isInBrowser())
1041
+ return;
1042
+ const tags = document.getElementsByTagName("link");
1043
+ for (var i = 0, tag; (tag = tags[i]); i++) {
1044
+ if (tag.getAttribute("rel") === "canonical") {
1045
+ return tag.getAttribute("href");
1046
+ }
1047
+ }
1048
+ }
1049
+ /**
1050
+ * bugged analytics.js logic that produces 'url' parameter by concating canonical URL with current search part
1051
+ * I produces broken results in some cases like SPA where path is changed but canonical URL is not updated
1052
+ */
1053
+ function analyticsJsUrl() {
1054
+ if (!isInBrowser())
1055
+ return;
1056
+ const canonical = canonicalUrl();
1057
+ if (!canonical)
1058
+ return window.location.href.replace(hashRegex, "");
1059
+ return canonical.match(/\?/) ? canonical : canonical + window.location.search;
1060
+ }
1017
1061
  function adjustPayload(payload, config, storage, s2s) {
1018
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
1062
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
1019
1063
  const runtime = config.runtime || (isInBrowser() ? windowRuntime(config) : emptyRuntime(config));
1020
1064
  const url = runtime.pageUrl();
1021
1065
  const parsedUrl = safeCall(() => new URL(url), undefined);
1022
1066
  const query = parsedUrl ? parseQuery(parsedUrl.search) : {};
1023
1067
  const properties = payload.properties || {};
1024
1068
  if (payload.type === "page" && (properties.url || url)) {
1025
- const targetUrl = properties.url || url;
1069
+ // we don't trust analytics.js URL logic since it's sticks with canonical URL on SPA pages
1070
+ let targetUrl = url || properties.url;
1071
+ if (properties.url && properties.url !== analyticsJsUrl()) {
1072
+ // properties.url is not the same as provided by analytics.js
1073
+ // it means that it was not overridden by user and we should use it
1074
+ targetUrl = properties.url;
1075
+ }
1026
1076
  properties.url = targetUrl.replace(hashRegex, "");
1027
1077
  properties.path = fixPath(urlPath(targetUrl));
1078
+ // other properties are correctly based on window.location in analytics.js
1028
1079
  }
1029
1080
  const customContext = deepMerge(config.defaultPayloadContext, ((_a = payload.properties) === null || _a === void 0 ? void 0 : _a.context) || ((_b = payload.options) === null || _b === void 0 ? void 0 : _b.context) || {});
1030
1081
  (_c = payload.properties) === null || _c === void 0 ? true : delete _c.context;
@@ -1035,17 +1086,17 @@ function adjustPayload(payload, config, storage, s2s) {
1035
1086
  version: jitsuVersion,
1036
1087
  env: isInBrowser() ? "browser" : "node",
1037
1088
  },
1038
- consent: ((_d = config.privacy) === null || _d === void 0 ? void 0 : _d.consentCategories)
1089
+ consent: typeof ((_d = config.privacy) === null || _d === void 0 ? void 0 : _d.consentCategories) === "object" && Object.keys((_e = config.privacy) === null || _e === void 0 ? void 0 : _e.consentCategories).length
1039
1090
  ? {
1040
1091
  categoryPreferences: config.privacy.consentCategories,
1041
1092
  }
1042
1093
  : undefined,
1043
- userAgent: (_e = runtime.userAgent) === null || _e === void 0 ? void 0 : _e.call(runtime),
1044
- locale: (_f = runtime.language) === null || _f === void 0 ? void 0 : _f.call(runtime),
1045
- screen: (_g = runtime.screen) === null || _g === void 0 ? void 0 : _g.call(runtime),
1046
- ip: (_h = runtime === null || runtime === void 0 ? void 0 : runtime.ip) === null || _h === void 0 ? void 0 : _h.call(runtime),
1094
+ userAgent: (_f = runtime.userAgent) === null || _f === void 0 ? void 0 : _f.call(runtime),
1095
+ locale: (_g = runtime.language) === null || _g === void 0 ? void 0 : _g.call(runtime),
1096
+ screen: (_h = runtime.screen) === null || _h === void 0 ? void 0 : _h.call(runtime),
1097
+ ip: (_j = runtime === null || runtime === void 0 ? void 0 : runtime.ip) === null || _j === void 0 ? void 0 : _j.call(runtime),
1047
1098
  traits: payload.type != "identify" && payload.type != "group"
1048
- ? Object.assign(Object.assign({}, (restoreTraits(storage) || {})), (((_j = payload === null || payload === void 0 ? void 0 : payload.options) === null || _j === void 0 ? void 0 : _j.traits) || {})) : undefined,
1099
+ ? Object.assign(Object.assign({}, (restoreTraits(storage) || {})), (((_k = payload === null || payload === void 0 ? void 0 : payload.options) === null || _k === void 0 ? void 0 : _k.traits) || {})) : undefined,
1049
1100
  page: {
1050
1101
  path: properties.path || (parsedUrl && parsedUrl.pathname),
1051
1102
  referrer: referrer,
@@ -1056,13 +1107,13 @@ function adjustPayload(payload, config, storage, s2s) {
1056
1107
  url: properties.url || url,
1057
1108
  encoding: properties.encoding || runtime.documentEncoding(),
1058
1109
  },
1059
- clientIds: !((_k = config.privacy) === null || _k === void 0 ? void 0 : _k.disableUserIds) ? getClientIds(runtime, config.cookieCapture) : undefined,
1110
+ clientIds: !((_l = config.privacy) === null || _l === void 0 ? void 0 : _l.disableUserIds) ? getClientIds(runtime, config.cookieCapture) : undefined,
1060
1111
  campaign: parseUtms(query),
1061
1112
  };
1062
- const withContext = Object.assign(Object.assign({}, payload), { userId: ((_l = payload === null || payload === void 0 ? void 0 : payload.options) === null || _l === void 0 ? void 0 : _l.userId) || (payload === null || payload === void 0 ? void 0 : payload.userId), anonymousId: ((_m = payload === null || payload === void 0 ? void 0 : payload.options) === null || _m === void 0 ? void 0 : _m.anonymousId) || (payload === null || payload === void 0 ? void 0 : payload.anonymousId), groupId: ((_o = payload === null || payload === void 0 ? void 0 : payload.options) === null || _o === void 0 ? void 0 : _o.groupId) || storage.getItem("__group_id"), timestamp: new Date().toISOString(), sentAt: new Date().toISOString(), messageId: randomId(properties.path || (parsedUrl && parsedUrl.pathname)), writeKey: maskWriteKey(config.writeKey), context: deepMerge(context, customContext) });
1113
+ const withContext = Object.assign(Object.assign({}, payload), { userId: ((_m = payload === null || payload === void 0 ? void 0 : payload.options) === null || _m === void 0 ? void 0 : _m.userId) || (payload === null || payload === void 0 ? void 0 : payload.userId), anonymousId: ((_o = payload === null || payload === void 0 ? void 0 : payload.options) === null || _o === void 0 ? void 0 : _o.anonymousId) || (payload === null || payload === void 0 ? void 0 : payload.anonymousId), groupId: ((_p = payload === null || payload === void 0 ? void 0 : payload.options) === null || _p === void 0 ? void 0 : _p.groupId) || storage.getItem("__group_id"), timestamp: new Date().toISOString(), sentAt: new Date().toISOString(), messageId: randomId(properties.path || (parsedUrl && parsedUrl.pathname)), writeKey: maskWriteKey(config.writeKey), context: deepMerge(context, customContext) });
1063
1114
  delete withContext.meta;
1064
1115
  delete withContext.options;
1065
- if ((_p = config.privacy) === null || _p === void 0 ? void 0 : _p.disableUserIds) {
1116
+ if ((_q = config.privacy) === null || _q === void 0 ? void 0 : _q.disableUserIds) {
1066
1117
  delete withContext.userId;
1067
1118
  delete withContext.anonymousId;
1068
1119
  delete withContext.context.traits;
@@ -1078,15 +1129,18 @@ function processDestinations(destinations, method, originalEvent, debug, analyti
1078
1129
  return __awaiter$1(this, void 0, void 0, function* () {
1079
1130
  const promises = [];
1080
1131
  for (const destination of destinations) {
1081
- let newEvents = [originalEvent];
1132
+ let newEvents = [];
1082
1133
  if (destination.newEvents) {
1083
1134
  try {
1084
- newEvents = destination.newEvents.map(e => e === "same" ? originalEvent : isDiff(e) ? diff.patch(originalEvent, e.__diff) : e);
1135
+ newEvents = destination.newEvents.map(e => e === "same" ? deepCopy(originalEvent) : isDiff(e) ? diff.patch(deepCopy(originalEvent), e.__diff) : e);
1085
1136
  }
1086
1137
  catch (e) {
1087
1138
  console.error(`[JITSU] Error applying '${destination.id}' changes to event: ${e === null || e === void 0 ? void 0 : e.message}`, e);
1088
1139
  }
1089
1140
  }
1141
+ else {
1142
+ newEvents = [deepCopy(originalEvent)];
1143
+ }
1090
1144
  const credentials = Object.assign(Object.assign({}, destination.credentials), destination.options);
1091
1145
  if (destination.deviceOptions.type === "internal-plugin") {
1092
1146
  const plugin = internalDestinationPlugins[destination.deviceOptions.name];
@@ -1494,6 +1548,9 @@ const emptyAnalytics = {
1494
1548
  group: () => Promise.resolve({}),
1495
1549
  reset: () => Promise.resolve({}),
1496
1550
  configure: () => { },
1551
+ getConfiguration() {
1552
+ return {};
1553
+ },
1497
1554
  };
1498
1555
  function createUnderlyingAnalyticsInstance(opts, rt, plugins = []) {
1499
1556
  var _a, _b, _c;
@@ -1646,6 +1703,9 @@ function createUnderlyingAnalyticsInstance(opts, rt, plugins = []) {
1646
1703
  //However, since returned values are used for debugging purposes only, it's ok
1647
1704
  return results[0];
1648
1705
  });
1706
+ },
1707
+ getConfiguration() {
1708
+ return opts;
1649
1709
  } });
1650
1710
  if (((_b = opts.privacy) === null || _b === void 0 ? void 0 : _b.disableUserIds) || ((_c = opts.privacy) === null || _c === void 0 ? void 0 : _c.dontSend)) {
1651
1711
  storage.reset();
@@ -649,7 +649,7 @@ const defaultConfig = {
649
649
  dontSend: false,
650
650
  disableUserIds: false,
651
651
  ipPolicy: "keep",
652
- consentCategories: undefined,
652
+ consentCategories: {},
653
653
  },
654
654
  };
655
655
  // mergeConfig merges newConfig into currentConfig also making sure that all undefined values are replaced with defaultConfig values
@@ -658,11 +658,14 @@ const mergeConfig = (current, newConfig) => {
658
658
  const value = newConfig[key];
659
659
  if (key === "privacy") {
660
660
  if (typeof value === "object") {
661
- current.privacy = Object.assign(Object.assign(Object.assign({}, defaultConfig.privacy), current.privacy), value);
661
+ current.privacy = Object.assign(Object.assign(Object.assign(Object.assign({}, defaultConfig.privacy), { consentCategories: Object.assign({}, defaultConfig.privacy.consentCategories) }), current.privacy), value);
662
662
  }
663
- else if (newConfig.hasOwnProperty("privacy") && typeof value === "undefined") {
664
- // explicitly set to undefined - reset to default
665
- current.privacy = Object.assign({}, defaultConfig.privacy);
663
+ else if (typeof value === "undefined") {
664
+ if (newConfig.hasOwnProperty("privacy") || !current.hasOwnProperty("privacy")) {
665
+ // explicitly set to undefined - reset to default
666
+ // or was not set at all - set to default
667
+ current.privacy = Object.assign(Object.assign({}, defaultConfig.privacy), { consentCategories: Object.assign({}, defaultConfig.privacy.consentCategories) });
668
+ }
666
669
  }
667
670
  }
668
671
  else if (typeof value === "undefined") {
@@ -737,6 +740,19 @@ function getClientIds(runtime, customCookieCapture) {
737
740
  }, {});
738
741
  return Object.assign(Object.assign({}, clientIds), getGa4Ids(runtime));
739
742
  }
743
+ function parseGa4SessionId(cookieValue) {
744
+ if (typeof cookieValue !== "string") {
745
+ return undefined;
746
+ }
747
+ if (cookieValue.startsWith("GA1") || cookieValue.startsWith("GS1")) {
748
+ return cookieValue.split(".")[2];
749
+ }
750
+ else {
751
+ // parse new GA4 cookie format, e.g.: GS2.1.s1747323152$o28$g0$t1747323152$j60$l0$h69286059
752
+ const match = cookieValue.match(/^GS\d+\.\d+\.(?:[\w_-]+[$])*s(\d+)(?:$|[$])/);
753
+ return match ? match[1] : undefined;
754
+ }
755
+ }
740
756
  function getGa4Ids(runtime) {
741
757
  var _a;
742
758
  const allCookies = runtime.getCookies();
@@ -745,14 +761,11 @@ function getGa4Ids(runtime) {
745
761
  const sessionIds = gaSessionCookies.length > 0
746
762
  ? Object.fromEntries(gaSessionCookies
747
763
  .map(([key, value]) => {
748
- if (typeof value !== "string") {
749
- return null;
750
- }
751
- const parts = value.split(".");
752
- if (parts.length < 3) {
764
+ const sessionId = parseGa4SessionId(value);
765
+ if (!sessionId) {
753
766
  return null;
754
767
  }
755
- return [key.substring("_ga_".length), parts[2]];
768
+ return [key.substring("_ga_".length), sessionId];
756
769
  })
757
770
  .filter(v => v !== null))
758
771
  : undefined;
@@ -976,6 +989,15 @@ const emptyRuntime = (config) => ({
976
989
  return undefined;
977
990
  },
978
991
  });
992
+ function deepCopy(o) {
993
+ if (typeof o !== "object") {
994
+ return o;
995
+ }
996
+ if (!o) {
997
+ return o;
998
+ }
999
+ return JSON.parse(JSON.stringify(o));
1000
+ }
979
1001
  function deepMerge(target, source) {
980
1002
  if (typeof source !== "object" || source === null) {
981
1003
  return source;
@@ -1012,17 +1034,46 @@ function urlPath(url) {
1012
1034
  const pathMatch = matches && matches[3] ? matches[3].split("?")[0].replace(hashRegex, "") : "";
1013
1035
  return "/" + pathMatch;
1014
1036
  }
1037
+ function canonicalUrl() {
1038
+ if (!isInBrowser())
1039
+ return;
1040
+ const tags = document.getElementsByTagName("link");
1041
+ for (var i = 0, tag; (tag = tags[i]); i++) {
1042
+ if (tag.getAttribute("rel") === "canonical") {
1043
+ return tag.getAttribute("href");
1044
+ }
1045
+ }
1046
+ }
1047
+ /**
1048
+ * bugged analytics.js logic that produces 'url' parameter by concating canonical URL with current search part
1049
+ * I produces broken results in some cases like SPA where path is changed but canonical URL is not updated
1050
+ */
1051
+ function analyticsJsUrl() {
1052
+ if (!isInBrowser())
1053
+ return;
1054
+ const canonical = canonicalUrl();
1055
+ if (!canonical)
1056
+ return window.location.href.replace(hashRegex, "");
1057
+ return canonical.match(/\?/) ? canonical : canonical + window.location.search;
1058
+ }
1015
1059
  function adjustPayload(payload, config, storage, s2s) {
1016
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
1060
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
1017
1061
  const runtime = config.runtime || (isInBrowser() ? windowRuntime(config) : emptyRuntime(config));
1018
1062
  const url = runtime.pageUrl();
1019
1063
  const parsedUrl = safeCall(() => new URL(url), undefined);
1020
1064
  const query = parsedUrl ? parseQuery(parsedUrl.search) : {};
1021
1065
  const properties = payload.properties || {};
1022
1066
  if (payload.type === "page" && (properties.url || url)) {
1023
- const targetUrl = properties.url || url;
1067
+ // we don't trust analytics.js URL logic since it's sticks with canonical URL on SPA pages
1068
+ let targetUrl = url || properties.url;
1069
+ if (properties.url && properties.url !== analyticsJsUrl()) {
1070
+ // properties.url is not the same as provided by analytics.js
1071
+ // it means that it was not overridden by user and we should use it
1072
+ targetUrl = properties.url;
1073
+ }
1024
1074
  properties.url = targetUrl.replace(hashRegex, "");
1025
1075
  properties.path = fixPath(urlPath(targetUrl));
1076
+ // other properties are correctly based on window.location in analytics.js
1026
1077
  }
1027
1078
  const customContext = deepMerge(config.defaultPayloadContext, ((_a = payload.properties) === null || _a === void 0 ? void 0 : _a.context) || ((_b = payload.options) === null || _b === void 0 ? void 0 : _b.context) || {});
1028
1079
  (_c = payload.properties) === null || _c === void 0 ? true : delete _c.context;
@@ -1033,17 +1084,17 @@ function adjustPayload(payload, config, storage, s2s) {
1033
1084
  version: jitsuVersion,
1034
1085
  env: isInBrowser() ? "browser" : "node",
1035
1086
  },
1036
- consent: ((_d = config.privacy) === null || _d === void 0 ? void 0 : _d.consentCategories)
1087
+ consent: typeof ((_d = config.privacy) === null || _d === void 0 ? void 0 : _d.consentCategories) === "object" && Object.keys((_e = config.privacy) === null || _e === void 0 ? void 0 : _e.consentCategories).length
1037
1088
  ? {
1038
1089
  categoryPreferences: config.privacy.consentCategories,
1039
1090
  }
1040
1091
  : undefined,
1041
- userAgent: (_e = runtime.userAgent) === null || _e === void 0 ? void 0 : _e.call(runtime),
1042
- locale: (_f = runtime.language) === null || _f === void 0 ? void 0 : _f.call(runtime),
1043
- screen: (_g = runtime.screen) === null || _g === void 0 ? void 0 : _g.call(runtime),
1044
- ip: (_h = runtime === null || runtime === void 0 ? void 0 : runtime.ip) === null || _h === void 0 ? void 0 : _h.call(runtime),
1092
+ userAgent: (_f = runtime.userAgent) === null || _f === void 0 ? void 0 : _f.call(runtime),
1093
+ locale: (_g = runtime.language) === null || _g === void 0 ? void 0 : _g.call(runtime),
1094
+ screen: (_h = runtime.screen) === null || _h === void 0 ? void 0 : _h.call(runtime),
1095
+ ip: (_j = runtime === null || runtime === void 0 ? void 0 : runtime.ip) === null || _j === void 0 ? void 0 : _j.call(runtime),
1045
1096
  traits: payload.type != "identify" && payload.type != "group"
1046
- ? Object.assign(Object.assign({}, (restoreTraits(storage) || {})), (((_j = payload === null || payload === void 0 ? void 0 : payload.options) === null || _j === void 0 ? void 0 : _j.traits) || {})) : undefined,
1097
+ ? Object.assign(Object.assign({}, (restoreTraits(storage) || {})), (((_k = payload === null || payload === void 0 ? void 0 : payload.options) === null || _k === void 0 ? void 0 : _k.traits) || {})) : undefined,
1047
1098
  page: {
1048
1099
  path: properties.path || (parsedUrl && parsedUrl.pathname),
1049
1100
  referrer: referrer,
@@ -1054,13 +1105,13 @@ function adjustPayload(payload, config, storage, s2s) {
1054
1105
  url: properties.url || url,
1055
1106
  encoding: properties.encoding || runtime.documentEncoding(),
1056
1107
  },
1057
- clientIds: !((_k = config.privacy) === null || _k === void 0 ? void 0 : _k.disableUserIds) ? getClientIds(runtime, config.cookieCapture) : undefined,
1108
+ clientIds: !((_l = config.privacy) === null || _l === void 0 ? void 0 : _l.disableUserIds) ? getClientIds(runtime, config.cookieCapture) : undefined,
1058
1109
  campaign: parseUtms(query),
1059
1110
  };
1060
- const withContext = Object.assign(Object.assign({}, payload), { userId: ((_l = payload === null || payload === void 0 ? void 0 : payload.options) === null || _l === void 0 ? void 0 : _l.userId) || (payload === null || payload === void 0 ? void 0 : payload.userId), anonymousId: ((_m = payload === null || payload === void 0 ? void 0 : payload.options) === null || _m === void 0 ? void 0 : _m.anonymousId) || (payload === null || payload === void 0 ? void 0 : payload.anonymousId), groupId: ((_o = payload === null || payload === void 0 ? void 0 : payload.options) === null || _o === void 0 ? void 0 : _o.groupId) || storage.getItem("__group_id"), timestamp: new Date().toISOString(), sentAt: new Date().toISOString(), messageId: randomId(properties.path || (parsedUrl && parsedUrl.pathname)), writeKey: maskWriteKey(config.writeKey), context: deepMerge(context, customContext) });
1111
+ const withContext = Object.assign(Object.assign({}, payload), { userId: ((_m = payload === null || payload === void 0 ? void 0 : payload.options) === null || _m === void 0 ? void 0 : _m.userId) || (payload === null || payload === void 0 ? void 0 : payload.userId), anonymousId: ((_o = payload === null || payload === void 0 ? void 0 : payload.options) === null || _o === void 0 ? void 0 : _o.anonymousId) || (payload === null || payload === void 0 ? void 0 : payload.anonymousId), groupId: ((_p = payload === null || payload === void 0 ? void 0 : payload.options) === null || _p === void 0 ? void 0 : _p.groupId) || storage.getItem("__group_id"), timestamp: new Date().toISOString(), sentAt: new Date().toISOString(), messageId: randomId(properties.path || (parsedUrl && parsedUrl.pathname)), writeKey: maskWriteKey(config.writeKey), context: deepMerge(context, customContext) });
1061
1112
  delete withContext.meta;
1062
1113
  delete withContext.options;
1063
- if ((_p = config.privacy) === null || _p === void 0 ? void 0 : _p.disableUserIds) {
1114
+ if ((_q = config.privacy) === null || _q === void 0 ? void 0 : _q.disableUserIds) {
1064
1115
  delete withContext.userId;
1065
1116
  delete withContext.anonymousId;
1066
1117
  delete withContext.context.traits;
@@ -1076,15 +1127,18 @@ function processDestinations(destinations, method, originalEvent, debug, analyti
1076
1127
  return __awaiter$1(this, void 0, void 0, function* () {
1077
1128
  const promises = [];
1078
1129
  for (const destination of destinations) {
1079
- let newEvents = [originalEvent];
1130
+ let newEvents = [];
1080
1131
  if (destination.newEvents) {
1081
1132
  try {
1082
- newEvents = destination.newEvents.map(e => e === "same" ? originalEvent : isDiff(e) ? diff.patch(originalEvent, e.__diff) : e);
1133
+ newEvents = destination.newEvents.map(e => e === "same" ? deepCopy(originalEvent) : isDiff(e) ? diff.patch(deepCopy(originalEvent), e.__diff) : e);
1083
1134
  }
1084
1135
  catch (e) {
1085
1136
  console.error(`[JITSU] Error applying '${destination.id}' changes to event: ${e === null || e === void 0 ? void 0 : e.message}`, e);
1086
1137
  }
1087
1138
  }
1139
+ else {
1140
+ newEvents = [deepCopy(originalEvent)];
1141
+ }
1088
1142
  const credentials = Object.assign(Object.assign({}, destination.credentials), destination.options);
1089
1143
  if (destination.deviceOptions.type === "internal-plugin") {
1090
1144
  const plugin = internalDestinationPlugins[destination.deviceOptions.name];
@@ -1492,6 +1546,9 @@ const emptyAnalytics = {
1492
1546
  group: () => Promise.resolve({}),
1493
1547
  reset: () => Promise.resolve({}),
1494
1548
  configure: () => { },
1549
+ getConfiguration() {
1550
+ return {};
1551
+ },
1495
1552
  };
1496
1553
  function createUnderlyingAnalyticsInstance(opts, rt, plugins = []) {
1497
1554
  var _a, _b, _c;
@@ -1644,6 +1701,9 @@ function createUnderlyingAnalyticsInstance(opts, rt, plugins = []) {
1644
1701
  //However, since returned values are used for debugging purposes only, it's ok
1645
1702
  return results[0];
1646
1703
  });
1704
+ },
1705
+ getConfiguration() {
1706
+ return opts;
1647
1707
  } });
1648
1708
  if (((_b = opts.privacy) === null || _b === void 0 ? void 0 : _b.disableUserIds) || ((_c = opts.privacy) === null || _c === void 0 ? void 0 : _c.dontSend)) {
1649
1709
  storage.reset();
package/dist/jitsu.cjs.js CHANGED
@@ -1073,7 +1073,7 @@ const defaultConfig = {
1073
1073
  dontSend: false,
1074
1074
  disableUserIds: false,
1075
1075
  ipPolicy: "keep",
1076
- consentCategories: undefined,
1076
+ consentCategories: {},
1077
1077
  },
1078
1078
  };
1079
1079
  // mergeConfig merges newConfig into currentConfig also making sure that all undefined values are replaced with defaultConfig values
@@ -1082,11 +1082,14 @@ const mergeConfig = (current, newConfig) => {
1082
1082
  const value = newConfig[key];
1083
1083
  if (key === "privacy") {
1084
1084
  if (typeof value === "object") {
1085
- current.privacy = Object.assign(Object.assign(Object.assign({}, defaultConfig.privacy), current.privacy), value);
1085
+ current.privacy = Object.assign(Object.assign(Object.assign(Object.assign({}, defaultConfig.privacy), { consentCategories: Object.assign({}, defaultConfig.privacy.consentCategories) }), current.privacy), value);
1086
1086
  }
1087
- else if (newConfig.hasOwnProperty("privacy") && typeof value === "undefined") {
1088
- // explicitly set to undefined - reset to default
1089
- current.privacy = Object.assign({}, defaultConfig.privacy);
1087
+ else if (typeof value === "undefined") {
1088
+ if (newConfig.hasOwnProperty("privacy") || !current.hasOwnProperty("privacy")) {
1089
+ // explicitly set to undefined - reset to default
1090
+ // or was not set at all - set to default
1091
+ current.privacy = Object.assign(Object.assign({}, defaultConfig.privacy), { consentCategories: Object.assign({}, defaultConfig.privacy.consentCategories) });
1092
+ }
1090
1093
  }
1091
1094
  }
1092
1095
  else if (typeof value === "undefined") {
@@ -1161,6 +1164,19 @@ function getClientIds(runtime, customCookieCapture) {
1161
1164
  }, {});
1162
1165
  return Object.assign(Object.assign({}, clientIds), getGa4Ids(runtime));
1163
1166
  }
1167
+ function parseGa4SessionId(cookieValue) {
1168
+ if (typeof cookieValue !== "string") {
1169
+ return undefined;
1170
+ }
1171
+ if (cookieValue.startsWith("GA1") || cookieValue.startsWith("GS1")) {
1172
+ return cookieValue.split(".")[2];
1173
+ }
1174
+ else {
1175
+ // parse new GA4 cookie format, e.g.: GS2.1.s1747323152$o28$g0$t1747323152$j60$l0$h69286059
1176
+ const match = cookieValue.match(/^GS\d+\.\d+\.(?:[\w_-]+[$])*s(\d+)(?:$|[$])/);
1177
+ return match ? match[1] : undefined;
1178
+ }
1179
+ }
1164
1180
  function getGa4Ids(runtime) {
1165
1181
  var _a;
1166
1182
  const allCookies = runtime.getCookies();
@@ -1169,14 +1185,11 @@ function getGa4Ids(runtime) {
1169
1185
  const sessionIds = gaSessionCookies.length > 0
1170
1186
  ? Object.fromEntries(gaSessionCookies
1171
1187
  .map(([key, value]) => {
1172
- if (typeof value !== "string") {
1173
- return null;
1174
- }
1175
- const parts = value.split(".");
1176
- if (parts.length < 3) {
1188
+ const sessionId = parseGa4SessionId(value);
1189
+ if (!sessionId) {
1177
1190
  return null;
1178
1191
  }
1179
- return [key.substring("_ga_".length), parts[2]];
1192
+ return [key.substring("_ga_".length), sessionId];
1180
1193
  })
1181
1194
  .filter(v => v !== null))
1182
1195
  : undefined;
@@ -1400,6 +1413,15 @@ const emptyRuntime = (config) => ({
1400
1413
  return undefined;
1401
1414
  },
1402
1415
  });
1416
+ function deepCopy(o) {
1417
+ if (typeof o !== "object") {
1418
+ return o;
1419
+ }
1420
+ if (!o) {
1421
+ return o;
1422
+ }
1423
+ return JSON.parse(JSON.stringify(o));
1424
+ }
1403
1425
  function deepMerge(target, source) {
1404
1426
  if (typeof source !== "object" || source === null) {
1405
1427
  return source;
@@ -1436,17 +1458,46 @@ function urlPath(url) {
1436
1458
  const pathMatch = matches && matches[3] ? matches[3].split("?")[0].replace(hashRegex, "") : "";
1437
1459
  return "/" + pathMatch;
1438
1460
  }
1461
+ function canonicalUrl() {
1462
+ if (!isInBrowser())
1463
+ return;
1464
+ const tags = document.getElementsByTagName("link");
1465
+ for (var i = 0, tag; (tag = tags[i]); i++) {
1466
+ if (tag.getAttribute("rel") === "canonical") {
1467
+ return tag.getAttribute("href");
1468
+ }
1469
+ }
1470
+ }
1471
+ /**
1472
+ * bugged analytics.js logic that produces 'url' parameter by concating canonical URL with current search part
1473
+ * I produces broken results in some cases like SPA where path is changed but canonical URL is not updated
1474
+ */
1475
+ function analyticsJsUrl() {
1476
+ if (!isInBrowser())
1477
+ return;
1478
+ const canonical = canonicalUrl();
1479
+ if (!canonical)
1480
+ return window.location.href.replace(hashRegex, "");
1481
+ return canonical.match(/\?/) ? canonical : canonical + window.location.search;
1482
+ }
1439
1483
  function adjustPayload(payload, config, storage, s2s) {
1440
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
1484
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
1441
1485
  const runtime = config.runtime || (isInBrowser() ? windowRuntime(config) : emptyRuntime(config));
1442
1486
  const url = runtime.pageUrl();
1443
1487
  const parsedUrl = safeCall(() => new URL(url), undefined);
1444
1488
  const query = parsedUrl ? parseQuery(parsedUrl.search) : {};
1445
1489
  const properties = payload.properties || {};
1446
1490
  if (payload.type === "page" && (properties.url || url)) {
1447
- const targetUrl = properties.url || url;
1491
+ // we don't trust analytics.js URL logic since it's sticks with canonical URL on SPA pages
1492
+ let targetUrl = url || properties.url;
1493
+ if (properties.url && properties.url !== analyticsJsUrl()) {
1494
+ // properties.url is not the same as provided by analytics.js
1495
+ // it means that it was not overridden by user and we should use it
1496
+ targetUrl = properties.url;
1497
+ }
1448
1498
  properties.url = targetUrl.replace(hashRegex, "");
1449
1499
  properties.path = fixPath(urlPath(targetUrl));
1500
+ // other properties are correctly based on window.location in analytics.js
1450
1501
  }
1451
1502
  const customContext = deepMerge(config.defaultPayloadContext, ((_a = payload.properties) === null || _a === void 0 ? void 0 : _a.context) || ((_b = payload.options) === null || _b === void 0 ? void 0 : _b.context) || {});
1452
1503
  (_c = payload.properties) === null || _c === void 0 ? true : delete _c.context;
@@ -1457,17 +1508,17 @@ function adjustPayload(payload, config, storage, s2s) {
1457
1508
  version: jitsuVersion,
1458
1509
  env: isInBrowser() ? "browser" : "node",
1459
1510
  },
1460
- consent: ((_d = config.privacy) === null || _d === void 0 ? void 0 : _d.consentCategories)
1511
+ consent: typeof ((_d = config.privacy) === null || _d === void 0 ? void 0 : _d.consentCategories) === "object" && Object.keys((_e = config.privacy) === null || _e === void 0 ? void 0 : _e.consentCategories).length
1461
1512
  ? {
1462
1513
  categoryPreferences: config.privacy.consentCategories,
1463
1514
  }
1464
1515
  : undefined,
1465
- userAgent: (_e = runtime.userAgent) === null || _e === void 0 ? void 0 : _e.call(runtime),
1466
- locale: (_f = runtime.language) === null || _f === void 0 ? void 0 : _f.call(runtime),
1467
- screen: (_g = runtime.screen) === null || _g === void 0 ? void 0 : _g.call(runtime),
1468
- ip: (_h = runtime === null || runtime === void 0 ? void 0 : runtime.ip) === null || _h === void 0 ? void 0 : _h.call(runtime),
1516
+ userAgent: (_f = runtime.userAgent) === null || _f === void 0 ? void 0 : _f.call(runtime),
1517
+ locale: (_g = runtime.language) === null || _g === void 0 ? void 0 : _g.call(runtime),
1518
+ screen: (_h = runtime.screen) === null || _h === void 0 ? void 0 : _h.call(runtime),
1519
+ ip: (_j = runtime === null || runtime === void 0 ? void 0 : runtime.ip) === null || _j === void 0 ? void 0 : _j.call(runtime),
1469
1520
  traits: payload.type != "identify" && payload.type != "group"
1470
- ? Object.assign(Object.assign({}, (restoreTraits(storage) || {})), (((_j = payload === null || payload === void 0 ? void 0 : payload.options) === null || _j === void 0 ? void 0 : _j.traits) || {})) : undefined,
1521
+ ? Object.assign(Object.assign({}, (restoreTraits(storage) || {})), (((_k = payload === null || payload === void 0 ? void 0 : payload.options) === null || _k === void 0 ? void 0 : _k.traits) || {})) : undefined,
1471
1522
  page: {
1472
1523
  path: properties.path || (parsedUrl && parsedUrl.pathname),
1473
1524
  referrer: referrer,
@@ -1478,13 +1529,13 @@ function adjustPayload(payload, config, storage, s2s) {
1478
1529
  url: properties.url || url,
1479
1530
  encoding: properties.encoding || runtime.documentEncoding(),
1480
1531
  },
1481
- clientIds: !((_k = config.privacy) === null || _k === void 0 ? void 0 : _k.disableUserIds) ? getClientIds(runtime, config.cookieCapture) : undefined,
1532
+ clientIds: !((_l = config.privacy) === null || _l === void 0 ? void 0 : _l.disableUserIds) ? getClientIds(runtime, config.cookieCapture) : undefined,
1482
1533
  campaign: parseUtms(query),
1483
1534
  };
1484
- const withContext = Object.assign(Object.assign({}, payload), { userId: ((_l = payload === null || payload === void 0 ? void 0 : payload.options) === null || _l === void 0 ? void 0 : _l.userId) || (payload === null || payload === void 0 ? void 0 : payload.userId), anonymousId: ((_m = payload === null || payload === void 0 ? void 0 : payload.options) === null || _m === void 0 ? void 0 : _m.anonymousId) || (payload === null || payload === void 0 ? void 0 : payload.anonymousId), groupId: ((_o = payload === null || payload === void 0 ? void 0 : payload.options) === null || _o === void 0 ? void 0 : _o.groupId) || storage.getItem("__group_id"), timestamp: new Date().toISOString(), sentAt: new Date().toISOString(), messageId: randomId(properties.path || (parsedUrl && parsedUrl.pathname)), writeKey: maskWriteKey(config.writeKey), context: deepMerge(context, customContext) });
1535
+ const withContext = Object.assign(Object.assign({}, payload), { userId: ((_m = payload === null || payload === void 0 ? void 0 : payload.options) === null || _m === void 0 ? void 0 : _m.userId) || (payload === null || payload === void 0 ? void 0 : payload.userId), anonymousId: ((_o = payload === null || payload === void 0 ? void 0 : payload.options) === null || _o === void 0 ? void 0 : _o.anonymousId) || (payload === null || payload === void 0 ? void 0 : payload.anonymousId), groupId: ((_p = payload === null || payload === void 0 ? void 0 : payload.options) === null || _p === void 0 ? void 0 : _p.groupId) || storage.getItem("__group_id"), timestamp: new Date().toISOString(), sentAt: new Date().toISOString(), messageId: randomId(properties.path || (parsedUrl && parsedUrl.pathname)), writeKey: maskWriteKey(config.writeKey), context: deepMerge(context, customContext) });
1485
1536
  delete withContext.meta;
1486
1537
  delete withContext.options;
1487
- if ((_p = config.privacy) === null || _p === void 0 ? void 0 : _p.disableUserIds) {
1538
+ if ((_q = config.privacy) === null || _q === void 0 ? void 0 : _q.disableUserIds) {
1488
1539
  delete withContext.userId;
1489
1540
  delete withContext.anonymousId;
1490
1541
  delete withContext.context.traits;
@@ -1500,15 +1551,18 @@ function processDestinations(destinations, method, originalEvent, debug, analyti
1500
1551
  return __awaiter$1(this, void 0, void 0, function* () {
1501
1552
  const promises = [];
1502
1553
  for (const destination of destinations) {
1503
- let newEvents = [originalEvent];
1554
+ let newEvents = [];
1504
1555
  if (destination.newEvents) {
1505
1556
  try {
1506
- newEvents = destination.newEvents.map(e => e === "same" ? originalEvent : isDiff(e) ? diff.patch(originalEvent, e.__diff) : e);
1557
+ newEvents = destination.newEvents.map(e => e === "same" ? deepCopy(originalEvent) : isDiff(e) ? diff.patch(deepCopy(originalEvent), e.__diff) : e);
1507
1558
  }
1508
1559
  catch (e) {
1509
1560
  console.error(`[JITSU] Error applying '${destination.id}' changes to event: ${e === null || e === void 0 ? void 0 : e.message}`, e);
1510
1561
  }
1511
1562
  }
1563
+ else {
1564
+ newEvents = [deepCopy(originalEvent)];
1565
+ }
1512
1566
  const credentials = Object.assign(Object.assign({}, destination.credentials), destination.options);
1513
1567
  if (destination.deviceOptions.type === "internal-plugin") {
1514
1568
  const plugin = internalDestinationPlugins[destination.deviceOptions.name];
@@ -1916,6 +1970,9 @@ const emptyAnalytics = {
1916
1970
  group: () => Promise.resolve({}),
1917
1971
  reset: () => Promise.resolve({}),
1918
1972
  configure: () => { },
1973
+ getConfiguration() {
1974
+ return {};
1975
+ },
1919
1976
  };
1920
1977
  function createUnderlyingAnalyticsInstance(opts, rt, plugins = []) {
1921
1978
  var _a, _b, _c;
@@ -2068,6 +2125,9 @@ function createUnderlyingAnalyticsInstance(opts, rt, plugins = []) {
2068
2125
  //However, since returned values are used for debugging purposes only, it's ok
2069
2126
  return results[0];
2070
2127
  });
2128
+ },
2129
+ getConfiguration() {
2130
+ return opts;
2071
2131
  } });
2072
2132
  if (((_b = opts.privacy) === null || _b === void 0 ? void 0 : _b.disableUserIds) || ((_c = opts.privacy) === null || _c === void 0 ? void 0 : _c.dontSend)) {
2073
2133
  storage.reset();
package/dist/jitsu.es.js CHANGED
@@ -1071,7 +1071,7 @@ const defaultConfig = {
1071
1071
  dontSend: false,
1072
1072
  disableUserIds: false,
1073
1073
  ipPolicy: "keep",
1074
- consentCategories: undefined,
1074
+ consentCategories: {},
1075
1075
  },
1076
1076
  };
1077
1077
  // mergeConfig merges newConfig into currentConfig also making sure that all undefined values are replaced with defaultConfig values
@@ -1080,11 +1080,14 @@ const mergeConfig = (current, newConfig) => {
1080
1080
  const value = newConfig[key];
1081
1081
  if (key === "privacy") {
1082
1082
  if (typeof value === "object") {
1083
- current.privacy = Object.assign(Object.assign(Object.assign({}, defaultConfig.privacy), current.privacy), value);
1083
+ current.privacy = Object.assign(Object.assign(Object.assign(Object.assign({}, defaultConfig.privacy), { consentCategories: Object.assign({}, defaultConfig.privacy.consentCategories) }), current.privacy), value);
1084
1084
  }
1085
- else if (newConfig.hasOwnProperty("privacy") && typeof value === "undefined") {
1086
- // explicitly set to undefined - reset to default
1087
- current.privacy = Object.assign({}, defaultConfig.privacy);
1085
+ else if (typeof value === "undefined") {
1086
+ if (newConfig.hasOwnProperty("privacy") || !current.hasOwnProperty("privacy")) {
1087
+ // explicitly set to undefined - reset to default
1088
+ // or was not set at all - set to default
1089
+ current.privacy = Object.assign(Object.assign({}, defaultConfig.privacy), { consentCategories: Object.assign({}, defaultConfig.privacy.consentCategories) });
1090
+ }
1088
1091
  }
1089
1092
  }
1090
1093
  else if (typeof value === "undefined") {
@@ -1159,6 +1162,19 @@ function getClientIds(runtime, customCookieCapture) {
1159
1162
  }, {});
1160
1163
  return Object.assign(Object.assign({}, clientIds), getGa4Ids(runtime));
1161
1164
  }
1165
+ function parseGa4SessionId(cookieValue) {
1166
+ if (typeof cookieValue !== "string") {
1167
+ return undefined;
1168
+ }
1169
+ if (cookieValue.startsWith("GA1") || cookieValue.startsWith("GS1")) {
1170
+ return cookieValue.split(".")[2];
1171
+ }
1172
+ else {
1173
+ // parse new GA4 cookie format, e.g.: GS2.1.s1747323152$o28$g0$t1747323152$j60$l0$h69286059
1174
+ const match = cookieValue.match(/^GS\d+\.\d+\.(?:[\w_-]+[$])*s(\d+)(?:$|[$])/);
1175
+ return match ? match[1] : undefined;
1176
+ }
1177
+ }
1162
1178
  function getGa4Ids(runtime) {
1163
1179
  var _a;
1164
1180
  const allCookies = runtime.getCookies();
@@ -1167,14 +1183,11 @@ function getGa4Ids(runtime) {
1167
1183
  const sessionIds = gaSessionCookies.length > 0
1168
1184
  ? Object.fromEntries(gaSessionCookies
1169
1185
  .map(([key, value]) => {
1170
- if (typeof value !== "string") {
1171
- return null;
1172
- }
1173
- const parts = value.split(".");
1174
- if (parts.length < 3) {
1186
+ const sessionId = parseGa4SessionId(value);
1187
+ if (!sessionId) {
1175
1188
  return null;
1176
1189
  }
1177
- return [key.substring("_ga_".length), parts[2]];
1190
+ return [key.substring("_ga_".length), sessionId];
1178
1191
  })
1179
1192
  .filter(v => v !== null))
1180
1193
  : undefined;
@@ -1398,6 +1411,15 @@ const emptyRuntime = (config) => ({
1398
1411
  return undefined;
1399
1412
  },
1400
1413
  });
1414
+ function deepCopy(o) {
1415
+ if (typeof o !== "object") {
1416
+ return o;
1417
+ }
1418
+ if (!o) {
1419
+ return o;
1420
+ }
1421
+ return JSON.parse(JSON.stringify(o));
1422
+ }
1401
1423
  function deepMerge(target, source) {
1402
1424
  if (typeof source !== "object" || source === null) {
1403
1425
  return source;
@@ -1434,17 +1456,46 @@ function urlPath(url) {
1434
1456
  const pathMatch = matches && matches[3] ? matches[3].split("?")[0].replace(hashRegex, "") : "";
1435
1457
  return "/" + pathMatch;
1436
1458
  }
1459
+ function canonicalUrl() {
1460
+ if (!isInBrowser())
1461
+ return;
1462
+ const tags = document.getElementsByTagName("link");
1463
+ for (var i = 0, tag; (tag = tags[i]); i++) {
1464
+ if (tag.getAttribute("rel") === "canonical") {
1465
+ return tag.getAttribute("href");
1466
+ }
1467
+ }
1468
+ }
1469
+ /**
1470
+ * bugged analytics.js logic that produces 'url' parameter by concating canonical URL with current search part
1471
+ * I produces broken results in some cases like SPA where path is changed but canonical URL is not updated
1472
+ */
1473
+ function analyticsJsUrl() {
1474
+ if (!isInBrowser())
1475
+ return;
1476
+ const canonical = canonicalUrl();
1477
+ if (!canonical)
1478
+ return window.location.href.replace(hashRegex, "");
1479
+ return canonical.match(/\?/) ? canonical : canonical + window.location.search;
1480
+ }
1437
1481
  function adjustPayload(payload, config, storage, s2s) {
1438
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
1482
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
1439
1483
  const runtime = config.runtime || (isInBrowser() ? windowRuntime(config) : emptyRuntime(config));
1440
1484
  const url = runtime.pageUrl();
1441
1485
  const parsedUrl = safeCall(() => new URL(url), undefined);
1442
1486
  const query = parsedUrl ? parseQuery(parsedUrl.search) : {};
1443
1487
  const properties = payload.properties || {};
1444
1488
  if (payload.type === "page" && (properties.url || url)) {
1445
- const targetUrl = properties.url || url;
1489
+ // we don't trust analytics.js URL logic since it's sticks with canonical URL on SPA pages
1490
+ let targetUrl = url || properties.url;
1491
+ if (properties.url && properties.url !== analyticsJsUrl()) {
1492
+ // properties.url is not the same as provided by analytics.js
1493
+ // it means that it was not overridden by user and we should use it
1494
+ targetUrl = properties.url;
1495
+ }
1446
1496
  properties.url = targetUrl.replace(hashRegex, "");
1447
1497
  properties.path = fixPath(urlPath(targetUrl));
1498
+ // other properties are correctly based on window.location in analytics.js
1448
1499
  }
1449
1500
  const customContext = deepMerge(config.defaultPayloadContext, ((_a = payload.properties) === null || _a === void 0 ? void 0 : _a.context) || ((_b = payload.options) === null || _b === void 0 ? void 0 : _b.context) || {});
1450
1501
  (_c = payload.properties) === null || _c === void 0 ? true : delete _c.context;
@@ -1455,17 +1506,17 @@ function adjustPayload(payload, config, storage, s2s) {
1455
1506
  version: jitsuVersion,
1456
1507
  env: isInBrowser() ? "browser" : "node",
1457
1508
  },
1458
- consent: ((_d = config.privacy) === null || _d === void 0 ? void 0 : _d.consentCategories)
1509
+ consent: typeof ((_d = config.privacy) === null || _d === void 0 ? void 0 : _d.consentCategories) === "object" && Object.keys((_e = config.privacy) === null || _e === void 0 ? void 0 : _e.consentCategories).length
1459
1510
  ? {
1460
1511
  categoryPreferences: config.privacy.consentCategories,
1461
1512
  }
1462
1513
  : undefined,
1463
- userAgent: (_e = runtime.userAgent) === null || _e === void 0 ? void 0 : _e.call(runtime),
1464
- locale: (_f = runtime.language) === null || _f === void 0 ? void 0 : _f.call(runtime),
1465
- screen: (_g = runtime.screen) === null || _g === void 0 ? void 0 : _g.call(runtime),
1466
- ip: (_h = runtime === null || runtime === void 0 ? void 0 : runtime.ip) === null || _h === void 0 ? void 0 : _h.call(runtime),
1514
+ userAgent: (_f = runtime.userAgent) === null || _f === void 0 ? void 0 : _f.call(runtime),
1515
+ locale: (_g = runtime.language) === null || _g === void 0 ? void 0 : _g.call(runtime),
1516
+ screen: (_h = runtime.screen) === null || _h === void 0 ? void 0 : _h.call(runtime),
1517
+ ip: (_j = runtime === null || runtime === void 0 ? void 0 : runtime.ip) === null || _j === void 0 ? void 0 : _j.call(runtime),
1467
1518
  traits: payload.type != "identify" && payload.type != "group"
1468
- ? Object.assign(Object.assign({}, (restoreTraits(storage) || {})), (((_j = payload === null || payload === void 0 ? void 0 : payload.options) === null || _j === void 0 ? void 0 : _j.traits) || {})) : undefined,
1519
+ ? Object.assign(Object.assign({}, (restoreTraits(storage) || {})), (((_k = payload === null || payload === void 0 ? void 0 : payload.options) === null || _k === void 0 ? void 0 : _k.traits) || {})) : undefined,
1469
1520
  page: {
1470
1521
  path: properties.path || (parsedUrl && parsedUrl.pathname),
1471
1522
  referrer: referrer,
@@ -1476,13 +1527,13 @@ function adjustPayload(payload, config, storage, s2s) {
1476
1527
  url: properties.url || url,
1477
1528
  encoding: properties.encoding || runtime.documentEncoding(),
1478
1529
  },
1479
- clientIds: !((_k = config.privacy) === null || _k === void 0 ? void 0 : _k.disableUserIds) ? getClientIds(runtime, config.cookieCapture) : undefined,
1530
+ clientIds: !((_l = config.privacy) === null || _l === void 0 ? void 0 : _l.disableUserIds) ? getClientIds(runtime, config.cookieCapture) : undefined,
1480
1531
  campaign: parseUtms(query),
1481
1532
  };
1482
- const withContext = Object.assign(Object.assign({}, payload), { userId: ((_l = payload === null || payload === void 0 ? void 0 : payload.options) === null || _l === void 0 ? void 0 : _l.userId) || (payload === null || payload === void 0 ? void 0 : payload.userId), anonymousId: ((_m = payload === null || payload === void 0 ? void 0 : payload.options) === null || _m === void 0 ? void 0 : _m.anonymousId) || (payload === null || payload === void 0 ? void 0 : payload.anonymousId), groupId: ((_o = payload === null || payload === void 0 ? void 0 : payload.options) === null || _o === void 0 ? void 0 : _o.groupId) || storage.getItem("__group_id"), timestamp: new Date().toISOString(), sentAt: new Date().toISOString(), messageId: randomId(properties.path || (parsedUrl && parsedUrl.pathname)), writeKey: maskWriteKey(config.writeKey), context: deepMerge(context, customContext) });
1533
+ const withContext = Object.assign(Object.assign({}, payload), { userId: ((_m = payload === null || payload === void 0 ? void 0 : payload.options) === null || _m === void 0 ? void 0 : _m.userId) || (payload === null || payload === void 0 ? void 0 : payload.userId), anonymousId: ((_o = payload === null || payload === void 0 ? void 0 : payload.options) === null || _o === void 0 ? void 0 : _o.anonymousId) || (payload === null || payload === void 0 ? void 0 : payload.anonymousId), groupId: ((_p = payload === null || payload === void 0 ? void 0 : payload.options) === null || _p === void 0 ? void 0 : _p.groupId) || storage.getItem("__group_id"), timestamp: new Date().toISOString(), sentAt: new Date().toISOString(), messageId: randomId(properties.path || (parsedUrl && parsedUrl.pathname)), writeKey: maskWriteKey(config.writeKey), context: deepMerge(context, customContext) });
1483
1534
  delete withContext.meta;
1484
1535
  delete withContext.options;
1485
- if ((_p = config.privacy) === null || _p === void 0 ? void 0 : _p.disableUserIds) {
1536
+ if ((_q = config.privacy) === null || _q === void 0 ? void 0 : _q.disableUserIds) {
1486
1537
  delete withContext.userId;
1487
1538
  delete withContext.anonymousId;
1488
1539
  delete withContext.context.traits;
@@ -1498,15 +1549,18 @@ function processDestinations(destinations, method, originalEvent, debug, analyti
1498
1549
  return __awaiter$1(this, void 0, void 0, function* () {
1499
1550
  const promises = [];
1500
1551
  for (const destination of destinations) {
1501
- let newEvents = [originalEvent];
1552
+ let newEvents = [];
1502
1553
  if (destination.newEvents) {
1503
1554
  try {
1504
- newEvents = destination.newEvents.map(e => e === "same" ? originalEvent : isDiff(e) ? diff.patch(originalEvent, e.__diff) : e);
1555
+ newEvents = destination.newEvents.map(e => e === "same" ? deepCopy(originalEvent) : isDiff(e) ? diff.patch(deepCopy(originalEvent), e.__diff) : e);
1505
1556
  }
1506
1557
  catch (e) {
1507
1558
  console.error(`[JITSU] Error applying '${destination.id}' changes to event: ${e === null || e === void 0 ? void 0 : e.message}`, e);
1508
1559
  }
1509
1560
  }
1561
+ else {
1562
+ newEvents = [deepCopy(originalEvent)];
1563
+ }
1510
1564
  const credentials = Object.assign(Object.assign({}, destination.credentials), destination.options);
1511
1565
  if (destination.deviceOptions.type === "internal-plugin") {
1512
1566
  const plugin = internalDestinationPlugins[destination.deviceOptions.name];
@@ -1914,6 +1968,9 @@ const emptyAnalytics = {
1914
1968
  group: () => Promise.resolve({}),
1915
1969
  reset: () => Promise.resolve({}),
1916
1970
  configure: () => { },
1971
+ getConfiguration() {
1972
+ return {};
1973
+ },
1917
1974
  };
1918
1975
  function createUnderlyingAnalyticsInstance(opts, rt, plugins = []) {
1919
1976
  var _a, _b, _c;
@@ -2066,6 +2123,9 @@ function createUnderlyingAnalyticsInstance(opts, rt, plugins = []) {
2066
2123
  //However, since returned values are used for debugging purposes only, it's ok
2067
2124
  return results[0];
2068
2125
  });
2126
+ },
2127
+ getConfiguration() {
2128
+ return opts;
2069
2129
  } });
2070
2130
  if (((_b = opts.privacy) === null || _b === void 0 ? void 0 : _b.disableUserIds) || ((_c = opts.privacy) === null || _c === void 0 ? void 0 : _c.dontSend)) {
2071
2131
  storage.reset();
package/dist/web/p.js.txt CHANGED
@@ -1074,7 +1074,7 @@
1074
1074
  dontSend: false,
1075
1075
  disableUserIds: false,
1076
1076
  ipPolicy: "keep",
1077
- consentCategories: undefined,
1077
+ consentCategories: {},
1078
1078
  },
1079
1079
  };
1080
1080
  // mergeConfig merges newConfig into currentConfig also making sure that all undefined values are replaced with defaultConfig values
@@ -1083,11 +1083,14 @@
1083
1083
  const value = newConfig[key];
1084
1084
  if (key === "privacy") {
1085
1085
  if (typeof value === "object") {
1086
- current.privacy = Object.assign(Object.assign(Object.assign({}, defaultConfig.privacy), current.privacy), value);
1086
+ current.privacy = Object.assign(Object.assign(Object.assign(Object.assign({}, defaultConfig.privacy), { consentCategories: Object.assign({}, defaultConfig.privacy.consentCategories) }), current.privacy), value);
1087
1087
  }
1088
- else if (newConfig.hasOwnProperty("privacy") && typeof value === "undefined") {
1089
- // explicitly set to undefined - reset to default
1090
- current.privacy = Object.assign({}, defaultConfig.privacy);
1088
+ else if (typeof value === "undefined") {
1089
+ if (newConfig.hasOwnProperty("privacy") || !current.hasOwnProperty("privacy")) {
1090
+ // explicitly set to undefined - reset to default
1091
+ // or was not set at all - set to default
1092
+ current.privacy = Object.assign(Object.assign({}, defaultConfig.privacy), { consentCategories: Object.assign({}, defaultConfig.privacy.consentCategories) });
1093
+ }
1091
1094
  }
1092
1095
  }
1093
1096
  else if (typeof value === "undefined") {
@@ -1162,6 +1165,19 @@
1162
1165
  }, {});
1163
1166
  return Object.assign(Object.assign({}, clientIds), getGa4Ids(runtime));
1164
1167
  }
1168
+ function parseGa4SessionId(cookieValue) {
1169
+ if (typeof cookieValue !== "string") {
1170
+ return undefined;
1171
+ }
1172
+ if (cookieValue.startsWith("GA1") || cookieValue.startsWith("GS1")) {
1173
+ return cookieValue.split(".")[2];
1174
+ }
1175
+ else {
1176
+ // parse new GA4 cookie format, e.g.: GS2.1.s1747323152$o28$g0$t1747323152$j60$l0$h69286059
1177
+ const match = cookieValue.match(/^GS\d+\.\d+\.(?:[\w_-]+[$])*s(\d+)(?:$|[$])/);
1178
+ return match ? match[1] : undefined;
1179
+ }
1180
+ }
1165
1181
  function getGa4Ids(runtime) {
1166
1182
  var _a;
1167
1183
  const allCookies = runtime.getCookies();
@@ -1170,14 +1186,11 @@
1170
1186
  const sessionIds = gaSessionCookies.length > 0
1171
1187
  ? Object.fromEntries(gaSessionCookies
1172
1188
  .map(([key, value]) => {
1173
- if (typeof value !== "string") {
1174
- return null;
1175
- }
1176
- const parts = value.split(".");
1177
- if (parts.length < 3) {
1189
+ const sessionId = parseGa4SessionId(value);
1190
+ if (!sessionId) {
1178
1191
  return null;
1179
1192
  }
1180
- return [key.substring("_ga_".length), parts[2]];
1193
+ return [key.substring("_ga_".length), sessionId];
1181
1194
  })
1182
1195
  .filter(v => v !== null))
1183
1196
  : undefined;
@@ -1401,6 +1414,15 @@
1401
1414
  return undefined;
1402
1415
  },
1403
1416
  });
1417
+ function deepCopy(o) {
1418
+ if (typeof o !== "object") {
1419
+ return o;
1420
+ }
1421
+ if (!o) {
1422
+ return o;
1423
+ }
1424
+ return JSON.parse(JSON.stringify(o));
1425
+ }
1404
1426
  function deepMerge(target, source) {
1405
1427
  if (typeof source !== "object" || source === null) {
1406
1428
  return source;
@@ -1437,17 +1459,46 @@
1437
1459
  const pathMatch = matches && matches[3] ? matches[3].split("?")[0].replace(hashRegex, "") : "";
1438
1460
  return "/" + pathMatch;
1439
1461
  }
1462
+ function canonicalUrl() {
1463
+ if (!isInBrowser())
1464
+ return;
1465
+ const tags = document.getElementsByTagName("link");
1466
+ for (var i = 0, tag; (tag = tags[i]); i++) {
1467
+ if (tag.getAttribute("rel") === "canonical") {
1468
+ return tag.getAttribute("href");
1469
+ }
1470
+ }
1471
+ }
1472
+ /**
1473
+ * bugged analytics.js logic that produces 'url' parameter by concating canonical URL with current search part
1474
+ * I produces broken results in some cases like SPA where path is changed but canonical URL is not updated
1475
+ */
1476
+ function analyticsJsUrl() {
1477
+ if (!isInBrowser())
1478
+ return;
1479
+ const canonical = canonicalUrl();
1480
+ if (!canonical)
1481
+ return window.location.href.replace(hashRegex, "");
1482
+ return canonical.match(/\?/) ? canonical : canonical + window.location.search;
1483
+ }
1440
1484
  function adjustPayload(payload, config, storage, s2s) {
1441
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
1485
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
1442
1486
  const runtime = config.runtime || (isInBrowser() ? windowRuntime(config) : emptyRuntime(config));
1443
1487
  const url = runtime.pageUrl();
1444
1488
  const parsedUrl = safeCall(() => new URL(url), undefined);
1445
1489
  const query = parsedUrl ? parseQuery(parsedUrl.search) : {};
1446
1490
  const properties = payload.properties || {};
1447
1491
  if (payload.type === "page" && (properties.url || url)) {
1448
- const targetUrl = properties.url || url;
1492
+ // we don't trust analytics.js URL logic since it's sticks with canonical URL on SPA pages
1493
+ let targetUrl = url || properties.url;
1494
+ if (properties.url && properties.url !== analyticsJsUrl()) {
1495
+ // properties.url is not the same as provided by analytics.js
1496
+ // it means that it was not overridden by user and we should use it
1497
+ targetUrl = properties.url;
1498
+ }
1449
1499
  properties.url = targetUrl.replace(hashRegex, "");
1450
1500
  properties.path = fixPath(urlPath(targetUrl));
1501
+ // other properties are correctly based on window.location in analytics.js
1451
1502
  }
1452
1503
  const customContext = deepMerge(config.defaultPayloadContext, ((_a = payload.properties) === null || _a === void 0 ? void 0 : _a.context) || ((_b = payload.options) === null || _b === void 0 ? void 0 : _b.context) || {});
1453
1504
  (_c = payload.properties) === null || _c === void 0 ? true : delete _c.context;
@@ -1458,17 +1509,17 @@
1458
1509
  version: jitsuVersion,
1459
1510
  env: isInBrowser() ? "browser" : "node",
1460
1511
  },
1461
- consent: ((_d = config.privacy) === null || _d === void 0 ? void 0 : _d.consentCategories)
1512
+ consent: typeof ((_d = config.privacy) === null || _d === void 0 ? void 0 : _d.consentCategories) === "object" && Object.keys((_e = config.privacy) === null || _e === void 0 ? void 0 : _e.consentCategories).length
1462
1513
  ? {
1463
1514
  categoryPreferences: config.privacy.consentCategories,
1464
1515
  }
1465
1516
  : undefined,
1466
- userAgent: (_e = runtime.userAgent) === null || _e === void 0 ? void 0 : _e.call(runtime),
1467
- locale: (_f = runtime.language) === null || _f === void 0 ? void 0 : _f.call(runtime),
1468
- screen: (_g = runtime.screen) === null || _g === void 0 ? void 0 : _g.call(runtime),
1469
- ip: (_h = runtime === null || runtime === void 0 ? void 0 : runtime.ip) === null || _h === void 0 ? void 0 : _h.call(runtime),
1517
+ userAgent: (_f = runtime.userAgent) === null || _f === void 0 ? void 0 : _f.call(runtime),
1518
+ locale: (_g = runtime.language) === null || _g === void 0 ? void 0 : _g.call(runtime),
1519
+ screen: (_h = runtime.screen) === null || _h === void 0 ? void 0 : _h.call(runtime),
1520
+ ip: (_j = runtime === null || runtime === void 0 ? void 0 : runtime.ip) === null || _j === void 0 ? void 0 : _j.call(runtime),
1470
1521
  traits: payload.type != "identify" && payload.type != "group"
1471
- ? Object.assign(Object.assign({}, (restoreTraits(storage) || {})), (((_j = payload === null || payload === void 0 ? void 0 : payload.options) === null || _j === void 0 ? void 0 : _j.traits) || {})) : undefined,
1522
+ ? Object.assign(Object.assign({}, (restoreTraits(storage) || {})), (((_k = payload === null || payload === void 0 ? void 0 : payload.options) === null || _k === void 0 ? void 0 : _k.traits) || {})) : undefined,
1472
1523
  page: {
1473
1524
  path: properties.path || (parsedUrl && parsedUrl.pathname),
1474
1525
  referrer: referrer,
@@ -1479,13 +1530,13 @@
1479
1530
  url: properties.url || url,
1480
1531
  encoding: properties.encoding || runtime.documentEncoding(),
1481
1532
  },
1482
- clientIds: !((_k = config.privacy) === null || _k === void 0 ? void 0 : _k.disableUserIds) ? getClientIds(runtime, config.cookieCapture) : undefined,
1533
+ clientIds: !((_l = config.privacy) === null || _l === void 0 ? void 0 : _l.disableUserIds) ? getClientIds(runtime, config.cookieCapture) : undefined,
1483
1534
  campaign: parseUtms(query),
1484
1535
  };
1485
- const withContext = Object.assign(Object.assign({}, payload), { userId: ((_l = payload === null || payload === void 0 ? void 0 : payload.options) === null || _l === void 0 ? void 0 : _l.userId) || (payload === null || payload === void 0 ? void 0 : payload.userId), anonymousId: ((_m = payload === null || payload === void 0 ? void 0 : payload.options) === null || _m === void 0 ? void 0 : _m.anonymousId) || (payload === null || payload === void 0 ? void 0 : payload.anonymousId), groupId: ((_o = payload === null || payload === void 0 ? void 0 : payload.options) === null || _o === void 0 ? void 0 : _o.groupId) || storage.getItem("__group_id"), timestamp: new Date().toISOString(), sentAt: new Date().toISOString(), messageId: randomId(properties.path || (parsedUrl && parsedUrl.pathname)), writeKey: maskWriteKey(config.writeKey), context: deepMerge(context, customContext) });
1536
+ const withContext = Object.assign(Object.assign({}, payload), { userId: ((_m = payload === null || payload === void 0 ? void 0 : payload.options) === null || _m === void 0 ? void 0 : _m.userId) || (payload === null || payload === void 0 ? void 0 : payload.userId), anonymousId: ((_o = payload === null || payload === void 0 ? void 0 : payload.options) === null || _o === void 0 ? void 0 : _o.anonymousId) || (payload === null || payload === void 0 ? void 0 : payload.anonymousId), groupId: ((_p = payload === null || payload === void 0 ? void 0 : payload.options) === null || _p === void 0 ? void 0 : _p.groupId) || storage.getItem("__group_id"), timestamp: new Date().toISOString(), sentAt: new Date().toISOString(), messageId: randomId(properties.path || (parsedUrl && parsedUrl.pathname)), writeKey: maskWriteKey(config.writeKey), context: deepMerge(context, customContext) });
1486
1537
  delete withContext.meta;
1487
1538
  delete withContext.options;
1488
- if ((_p = config.privacy) === null || _p === void 0 ? void 0 : _p.disableUserIds) {
1539
+ if ((_q = config.privacy) === null || _q === void 0 ? void 0 : _q.disableUserIds) {
1489
1540
  delete withContext.userId;
1490
1541
  delete withContext.anonymousId;
1491
1542
  delete withContext.context.traits;
@@ -1501,15 +1552,18 @@
1501
1552
  return __awaiter$1(this, void 0, void 0, function* () {
1502
1553
  const promises = [];
1503
1554
  for (const destination of destinations) {
1504
- let newEvents = [originalEvent];
1555
+ let newEvents = [];
1505
1556
  if (destination.newEvents) {
1506
1557
  try {
1507
- newEvents = destination.newEvents.map(e => e === "same" ? originalEvent : isDiff(e) ? diff.patch(originalEvent, e.__diff) : e);
1558
+ newEvents = destination.newEvents.map(e => e === "same" ? deepCopy(originalEvent) : isDiff(e) ? diff.patch(deepCopy(originalEvent), e.__diff) : e);
1508
1559
  }
1509
1560
  catch (e) {
1510
1561
  console.error(`[JITSU] Error applying '${destination.id}' changes to event: ${e === null || e === void 0 ? void 0 : e.message}`, e);
1511
1562
  }
1512
1563
  }
1564
+ else {
1565
+ newEvents = [deepCopy(originalEvent)];
1566
+ }
1513
1567
  const credentials = Object.assign(Object.assign({}, destination.credentials), destination.options);
1514
1568
  if (destination.deviceOptions.type === "internal-plugin") {
1515
1569
  const plugin = internalDestinationPlugins[destination.deviceOptions.name];
@@ -2055,6 +2109,9 @@
2055
2109
  //However, since returned values are used for debugging purposes only, it's ok
2056
2110
  return results[0];
2057
2111
  });
2112
+ },
2113
+ getConfiguration() {
2114
+ return opts;
2058
2115
  } });
2059
2116
  if (((_b = opts.privacy) === null || _b === void 0 ? void 0 : _b.disableUserIds) || ((_c = opts.privacy) === null || _c === void 0 ? void 0 : _c.dontSend)) {
2060
2117
  storage.reset();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jitsu/js",
3
- "version": "1.10.2",
3
+ "version": "1.10.4",
4
4
  "description": "",
5
5
  "author": "Jitsu Dev Team <dev@jitsu.com>",
6
6
  "main": "dist/jitsu.cjs.js",
@@ -39,10 +39,10 @@
39
39
  "rollup": "^3.29.5",
40
40
  "ts-jest": "29.0.5",
41
41
  "typescript": "^5.6.3",
42
- "jsondiffpatch": "1.10.2"
42
+ "jsondiffpatch": "1.10.4"
43
43
  },
44
44
  "peerDependencies": {
45
- "@jitsu/protocols": "1.10.2"
45
+ "@jitsu/protocols": "1.10.4"
46
46
  },
47
47
  "dependencies": {
48
48
  "analytics": "0.8.9"