@xapp/chat-widget 1.110.0 → 1.111.1

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.js CHANGED
@@ -3616,6 +3616,115 @@ function stripQueryString(url) {
3616
3616
  return undefined;
3617
3617
  }
3618
3618
  }
3619
+
3620
+ /**
3621
+ * Google Analytics 4 ids for chat leads.
3622
+ *
3623
+ * A form lead's `generate_lead` can go out from the browser, but a chat lead is only
3624
+ * ever sent by the backend's Measurement Protocol integration. Without the visitor's
3625
+ * GA client and session ids that event matches no browser session, and GA4 credits
3626
+ * it to Direct / Unassigned. These are sent as chat attributes so the backend can
3627
+ * credit the lead to the traffic source that brought the visitor.
3628
+ *
3629
+ * Only the ids GA itself set are read; nothing here is PII.
3630
+ */
3631
+ var SESSION_COOKIE_PREFIX = "_ga_";
3632
+ /**
3633
+ * Extracts the GA4 client id from a `_ga` cookie value: `GA1.1.<rand>.<ts>` ->
3634
+ * `<rand>.<ts>`.
3635
+ */
3636
+ function parseGaClientId(cookie) {
3637
+ var parts = (cookie === null || cookie === void 0 ? void 0 : cookie.split(".")) || [];
3638
+ if (parts.length < 4) {
3639
+ return undefined;
3640
+ }
3641
+ var clientId = parts.slice(-2).join(".");
3642
+ return /^\d+\.\d+$/.test(clientId) ? clientId : undefined;
3643
+ }
3644
+ /**
3645
+ * Extracts the session id from a `_ga_<stream>` cookie value. Handles both the legacy
3646
+ * `GS1.1.<sid>.<n>...` format and the 2025 `GS2.1.s<sid>$o3$...` format.
3647
+ */
3648
+ function parseGaSessionId(cookie) {
3649
+ var parts = (cookie === null || cookie === void 0 ? void 0 : cookie.split(".")) || [];
3650
+ if (parts.length < 3) {
3651
+ return undefined;
3652
+ }
3653
+ var sessionId;
3654
+ if (parts[0] === "GS2") {
3655
+ var token = parts[2].split("$").find(function (t) { return t.startsWith("s"); });
3656
+ sessionId = token === null || token === void 0 ? void 0 : token.slice(1);
3657
+ }
3658
+ else {
3659
+ sessionId = parts[2];
3660
+ }
3661
+ return sessionId && /^\d+$/.test(sessionId) ? sessionId : undefined;
3662
+ }
3663
+ function hasGlobalPrivacyControlOptOut() {
3664
+ if (typeof navigator === "undefined") {
3665
+ return false;
3666
+ }
3667
+ return (navigator.globalPrivacyControl === true);
3668
+ }
3669
+ function readCookies() {
3670
+ var cookies = {};
3671
+ if (typeof document === "undefined" || !document.cookie) {
3672
+ return cookies;
3673
+ }
3674
+ for (var _i = 0, _a = document.cookie.split(";"); _i < _a.length; _i++) {
3675
+ var pair = _a[_i];
3676
+ var index = pair.indexOf("=");
3677
+ if (index < 0) {
3678
+ continue;
3679
+ }
3680
+ var name_1 = pair.slice(0, index).trim();
3681
+ var raw = pair.slice(index + 1).trim();
3682
+ try {
3683
+ cookies[name_1] = decodeURIComponent(raw);
3684
+ }
3685
+ catch (_b) {
3686
+ // A stray "%" from another script must not break the chat.
3687
+ cookies[name_1] = raw;
3688
+ }
3689
+ }
3690
+ return cookies;
3691
+ }
3692
+ /**
3693
+ * The GA4 attributes to send with a chat message. Under Global Privacy Control only
3694
+ * the opt-out flag is sent. Never throws.
3695
+ */
3696
+ function getGa4Attributes() {
3697
+ try {
3698
+ if (hasGlobalPrivacyControlOptOut()) {
3699
+ return { ga4OptOut: true };
3700
+ }
3701
+ var cookies = readCookies();
3702
+ var attributes = {};
3703
+ var gaClientId = parseGaClientId(cookies["_ga"]);
3704
+ if (gaClientId) {
3705
+ attributes.gaClientId = gaClientId;
3706
+ }
3707
+ var gaSessionIds = {};
3708
+ for (var _i = 0, _a = Object.entries(cookies); _i < _a.length; _i++) {
3709
+ var _b = _a[_i], name_2 = _b[0], value = _b[1];
3710
+ if (!name_2.startsWith(SESSION_COOKIE_PREFIX)) {
3711
+ continue;
3712
+ }
3713
+ var stream = name_2.slice(SESSION_COOKIE_PREFIX.length);
3714
+ var sessionId = parseGaSessionId(value);
3715
+ if (/^[A-Z0-9]+$/i.test(stream) && sessionId) {
3716
+ gaSessionIds[stream] = sessionId;
3717
+ }
3718
+ }
3719
+ if (Object.keys(gaSessionIds).length > 0) {
3720
+ attributes.gaSessionIds = gaSessionIds;
3721
+ }
3722
+ return attributes;
3723
+ }
3724
+ catch (_c) {
3725
+ return {};
3726
+ }
3727
+ }
3619
3728
  /**
3620
3729
  * A minimal client for the tenant's homeowner MCP endpoint: one `tools/call` per request, no
3621
3730
  * session. The endpoint is public and the server keeps no state we need, so the MCP
@@ -4001,6 +4110,7 @@ var SuggestionsList_1 = SuggestionsList;
4001
4110
  var addTestLeadAttribute_1 = addTestLeadAttribute;
4002
4111
  var findItemByIndex_1 = findItemByIndex;
4003
4112
  var getAttribution_1 = getAttribution;
4113
+ var getGa4Attributes_1 = getGa4Attributes;
4004
4114
  var getItemsLength_1 = getItemsLength;
4005
4115
  var parseSuggestionsResponse_1 = parseSuggestionsResponse;
4006
4116
  var stripQueryString_1 = stripQueryString;
@@ -6579,16 +6689,25 @@ var StentorDirectChat = /** @class */ (function () {
6579
6689
  StentorDirectChat.prototype.wakeup = function () { };
6580
6690
  StentorDirectChat.prototype.postMessage = function (message) {
6581
6691
  return __awaiter$1(this, void 0, void 0, function () {
6582
- var request, userId, sessionId, accessToken, attributes, rwgToken, merchantId, environment, enablePreferredTime, service, origin, now, permissionRequest, expired, text, granted, userProfile, isEmail, configurableMessages, botResponse, successResult, success, fail, i, timeout, responseMessage;
6692
+ var request, userId, sessionId, accessToken, attributes, _i, _a, _b, key, value, rwgToken, merchantId, environment, enablePreferredTime, service, origin, now, permissionRequest, expired, text, granted, userProfile, isEmail, configurableMessages, botResponse, successResult, success, fail, i, timeout, responseMessage;
6583
6693
  var _this = this;
6584
- var _a, _b, _c, _d, _e;
6585
- return __generator$1(this, function (_f) {
6586
- switch (_f.label) {
6694
+ var _c, _d, _e, _f, _g, _h;
6695
+ return __generator$1(this, function (_j) {
6696
+ switch (_j.label) {
6587
6697
  case 0:
6588
6698
  userId = this._userId;
6589
6699
  sessionId = this._sessionId;
6590
6700
  accessToken = this._accessToken;
6591
6701
  attributes = mergeAttribution(this._attributes, getAttribution_1());
6702
+ // The visitor's GA client and session ids, so the backend can credit a chat lead
6703
+ // to the traffic source that brought them rather than Direct / Unassigned. Like the
6704
+ // rest of the attribution, a value the host page set itself wins.
6705
+ for (_i = 0, _a = Object.entries(getGa4Attributes_1()); _i < _a.length; _i++) {
6706
+ _b = _a[_i], key = _b[0], value = _b[1];
6707
+ if (((_c = this._attributes) === null || _c === void 0 ? void 0 : _c[key]) === undefined) {
6708
+ attributes[key] = value;
6709
+ }
6710
+ }
6592
6711
  rwgToken = localStorage.getItem("xa_rwg_token");
6593
6712
  if (rwgToken) {
6594
6713
  attributes["rwg_token"] = rwgToken;
@@ -6618,10 +6737,10 @@ var StentorDirectChat = /** @class */ (function () {
6618
6737
  // Add current time for accurate date/time responses
6619
6738
  attributes["currentTime"] = new Date().toISOString();
6620
6739
  now = new Date().getTime();
6621
- if (this.isNewSession && !((_a = message === null || message === void 0 ? void 0 : message.msg) === null || _a === void 0 ? void 0 : _a.text)) {
6740
+ if (this.isNewSession && !((_d = message === null || message === void 0 ? void 0 : message.msg) === null || _d === void 0 ? void 0 : _d.text)) {
6622
6741
  request = {
6623
6742
  type: "LAUNCH_REQUEST",
6624
- rawQuery: (_b = message === null || message === void 0 ? void 0 : message.msg) === null || _b === void 0 ? void 0 : _b.text,
6743
+ rawQuery: (_e = message === null || message === void 0 ? void 0 : message.msg) === null || _e === void 0 ? void 0 : _e.text,
6625
6744
  sessionId: sessionId,
6626
6745
  userId: userId,
6627
6746
  isNewSession: true,
@@ -6633,11 +6752,11 @@ var StentorDirectChat = /** @class */ (function () {
6633
6752
  };
6634
6753
  }
6635
6754
  else {
6636
- permissionRequest = (_c = message.msg) === null || _c === void 0 ? void 0 : _c.permissionRequest;
6755
+ permissionRequest = (_f = message.msg) === null || _f === void 0 ? void 0 : _f.permissionRequest;
6637
6756
  if (permissionRequest) {
6638
6757
  message = __assign$1(__assign$1({}, message), { msg: __assign$1(__assign$1({}, message.msg), { permissionRequest: undefined }) });
6639
6758
  expired = now - permissionRequest.time > PERMISSION_QUESTION_EXPIRATION_MS$1;
6640
- text = (_d = message.msg.text) === null || _d === void 0 ? void 0 : _d.toLowerCase();
6759
+ text = (_g = message.msg.text) === null || _g === void 0 ? void 0 : _g.toLowerCase();
6641
6760
  granted = !expired;
6642
6761
  userProfile = { id: this._userId };
6643
6762
  if (!expired) {
@@ -6735,7 +6854,7 @@ var StentorDirectChat = /** @class */ (function () {
6735
6854
  });
6736
6855
  }); };
6737
6856
  i = 1;
6738
- _f.label = 1;
6857
+ _j.label = 1;
6739
6858
  case 1:
6740
6859
  if (!(i < configurableMessages.length)) return [3 /*break*/, 4];
6741
6860
  timeout = this.config.timeout || 22000;
@@ -6751,12 +6870,12 @@ var StentorDirectChat = /** @class */ (function () {
6751
6870
  .catch(fail.bind(this, i))];
6752
6871
  case 2:
6753
6872
  // We don't start typing IF we have a failure message displayed and we are waiting on the timeout
6754
- _f.sent();
6873
+ _j.sent();
6755
6874
  // If we have what we want, break the for loop
6756
6875
  if (successResult) {
6757
6876
  return [3 /*break*/, 4];
6758
6877
  }
6759
- _f.label = 3;
6878
+ _j.label = 3;
6760
6879
  case 3:
6761
6880
  i++;
6762
6881
  return [3 /*break*/, 1];
@@ -6782,7 +6901,7 @@ var StentorDirectChat = /** @class */ (function () {
6782
6901
  : "The assistant is not responding right now. Please close the widget and open it again.",
6783
6902
  };
6784
6903
  }
6785
- if (typeof ((_e = this.hooks) === null || _e === void 0 ? void 0 : _e.onResponse) === "function") {
6904
+ if (typeof ((_h = this.hooks) === null || _h === void 0 ? void 0 : _h.onResponse) === "function") {
6786
6905
  this.hooks.onResponse(botResponse);
6787
6906
  }
6788
6907
  this.stopTyping();
@@ -7019,10 +7138,28 @@ function requestToMessage(botRequest, now) {
7019
7138
  }
7020
7139
 
7021
7140
  var ROUTER_USER = "router";
7141
+ /**
7142
+ * Shown when the server reports an error outside the router protocol and no failure
7143
+ * messages are configured.
7144
+ */
7145
+ var DEFAULT_GATEWAY_ERROR_TEXT = "Sorry, we cannot communicate with the assistant right now. Please check your connection or try again later.";
7146
+ function isGatewayErrorFrame(frame) {
7147
+ if (!frame || typeof frame !== "object") {
7148
+ return false;
7149
+ }
7150
+ var f = frame;
7151
+ return f.event === undefined && typeof f.message === "string";
7152
+ }
7022
7153
  var serverInfo = {
7023
7154
  deviceId: "Router",
7024
7155
  userId: ROUTER_USER,
7025
- displayName: "XAPP Server"};
7156
+ isAdmin: false,
7157
+ displayName: "XAPP Server",
7158
+ urlAttributes: {
7159
+ path: [],
7160
+ query: {}
7161
+ }
7162
+ };
7026
7163
  var StentorRouterChat = /** @class */ (function () {
7027
7164
  function StentorRouterChat(config, options) {
7028
7165
  var _a, _b, _c, _d, _e;
@@ -7035,6 +7172,8 @@ var StentorRouterChat = /** @class */ (function () {
7035
7172
  this.isAdmin = false;
7036
7173
  this.urlAttributes = {};
7037
7174
  this.handlers = {};
7175
+ // Senders whose typing indicator is currently on, so it can be cleared when a request fails
7176
+ this.typingSenders = new Map();
7038
7177
  // Sleep delay management
7039
7178
  // Use number | NodeJS.Timeout for cross-environment compatibility (browser vs Node.js)
7040
7179
  this.sleepTimeoutId = null;
@@ -7218,6 +7357,7 @@ var StentorRouterChat = /** @class */ (function () {
7218
7357
  });
7219
7358
  };
7220
7359
  this.handlers["typing"] = function (_data, sender, ts, _attributes) {
7360
+ _this.typingSenders.set(senderToNick(sender), sender);
7221
7361
  _this.dispatch({
7222
7362
  type: "chat",
7223
7363
  detail: {
@@ -7231,6 +7371,7 @@ var StentorRouterChat = /** @class */ (function () {
7231
7371
  });
7232
7372
  };
7233
7373
  this.handlers["stop typing"] = function (_data, sender, ts, _attributes) {
7374
+ _this.typingSenders.delete(senderToNick(sender));
7234
7375
  _this.dispatch({
7235
7376
  type: "chat",
7236
7377
  detail: {
@@ -7471,7 +7612,18 @@ var StentorRouterChat = /** @class */ (function () {
7471
7612
  };
7472
7613
  this.ws.onmessage = function (me) {
7473
7614
  log("ROUTER says: ".concat(me.data));
7474
- var message = JSON.parse(me.data);
7615
+ var message;
7616
+ try {
7617
+ message = JSON.parse(me.data);
7618
+ }
7619
+ catch (e) {
7620
+ err("Unparseable router message: ".concat(me.data));
7621
+ return;
7622
+ }
7623
+ if (isGatewayErrorFrame(message)) {
7624
+ _this.handleGatewayError(message);
7625
+ return;
7626
+ }
7475
7627
  var handler = _this.handlers[message.event];
7476
7628
  if (handler) {
7477
7629
  _this.handlers[message.event](message.data, message.sender, message.timeMs, message.attributes, message.messageId);
@@ -7580,6 +7732,32 @@ var StentorRouterChat = /** @class */ (function () {
7580
7732
  }
7581
7733
  });
7582
7734
  };
7735
+ /**
7736
+ * The gateway answered instead of the router (typically a Lambda timeout). The request is
7737
+ * lost and nothing else will arrive for it, so stop any typing indicator and tell the visitor.
7738
+ */
7739
+ StentorRouterChat.prototype.handleGatewayError = function (frame) {
7740
+ var _this = this;
7741
+ err("SERVER: gateway error: \"".concat(frame.message, "\" (requestId: ").concat(frame.requestId || "?", ")"));
7742
+ this.typingSenders.forEach(function (sender) {
7743
+ _this.dispatch({
7744
+ type: "chat",
7745
+ detail: {
7746
+ type: "chat.typing",
7747
+ user: {
7748
+ nick: senderToNick(sender),
7749
+ },
7750
+ typing: false,
7751
+ timestamp: +new Date(),
7752
+ },
7753
+ });
7754
+ });
7755
+ this.typingSenders.clear();
7756
+ // The last configured message is the final, no-retry one
7757
+ var last = this.configurableMessages[this.configurableMessages.length - 1];
7758
+ var configured = last && last.text ? last : undefined;
7759
+ this.sendFailureMessage(configured ? configured.retry : 1, 0, configured ? configured.text : DEFAULT_GATEWAY_ERROR_TEXT, serverInfo);
7760
+ };
7583
7761
  StentorRouterChat.prototype.sendDisconnectMessage = function () {
7584
7762
  this.dispatch({
7585
7763
  type: "chat",
@@ -16089,7 +16267,7 @@ const createQueueWithTimer = (timeout) => {
16089
16267
  setTimeout(notify, timeout);
16090
16268
  };
16091
16269
  };
16092
- const createRafWithFallbackTimer = (raf, timeout) => {
16270
+ const createRafWithFallbackTimer = (raf, caf, timeout) => {
16093
16271
  return (notify) => {
16094
16272
  let called = false;
16095
16273
  let rafId;
@@ -16097,7 +16275,7 @@ const createRafWithFallbackTimer = (raf, timeout) => {
16097
16275
  const callback = () => {
16098
16276
  if (called) return;
16099
16277
  called = true;
16100
- cancelAnimationFrame(rafId);
16278
+ caf?.(rafId);
16101
16279
  clearTimeout(timerId);
16102
16280
  notify();
16103
16281
  };
@@ -16111,7 +16289,7 @@ const autoBatchEnhancer = (options = { type: "raf" }) => (next) => (...args) =>
16111
16289
  let shouldNotifyAtEndOfTick = false;
16112
16290
  let notificationQueued = false;
16113
16291
  const listeners = /* @__PURE__ */ new Set();
16114
- const queueCallback = options.type === "tick" ? queueMicrotask : options.type === "raf" ? typeof window !== "undefined" && window.requestAnimationFrame ? createRafWithFallbackTimer(window.requestAnimationFrame, 100) : createQueueWithTimer(10) : options.type === "callback" ? options.queueNotification : createQueueWithTimer(options.timeout);
16292
+ const queueCallback = options.type === "tick" ? queueMicrotask : options.type === "raf" ? typeof window !== "undefined" && window.requestAnimationFrame ? createRafWithFallbackTimer(window.requestAnimationFrame, window.cancelAnimationFrame, 100) : createQueueWithTimer(10) : options.type === "callback" ? options.queueNotification : createQueueWithTimer(options.timeout);
16115
16293
  const notifyListeners = () => {
16116
16294
  notificationQueued = false;
16117
16295
  if (shouldNotifyAtEndOfTick) {