@usero/sdk 1.1.1 → 1.1.3

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/vanilla.cjs CHANGED
@@ -159,7 +159,11 @@ function getGradientEnd(color) {
159
159
 
160
160
  // src/identity.ts
161
161
  var ANON_STORAGE_KEY = "usero:anonymous-id";
162
+ var SDK_SESSION_STORAGE_KEY = "usero:session-replay:sdk-session-id";
162
163
  var cachedAnonymousId = null;
164
+ var cachedSdkSessionId = null;
165
+ var currentUserId = null;
166
+ var replayStartMs = null;
163
167
  var lastIdentifyFingerprint = null;
164
168
  function generateRandomId() {
165
169
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
@@ -190,6 +194,21 @@ function safeWriteLocalStorage(key, value) {
190
194
  } catch {
191
195
  }
192
196
  }
197
+ function safeReadSessionStorage(key) {
198
+ if (typeof window === "undefined") return null;
199
+ try {
200
+ return window.sessionStorage?.getItem(key) ?? null;
201
+ } catch {
202
+ return null;
203
+ }
204
+ }
205
+ function safeWriteSessionStorage(key, value) {
206
+ if (typeof window === "undefined") return;
207
+ try {
208
+ window.sessionStorage?.setItem(key, value);
209
+ } catch {
210
+ }
211
+ }
193
212
  function getOrMintAnonymousId() {
194
213
  if (cachedAnonymousId) return cachedAnonymousId;
195
214
  const existing = safeReadLocalStorage(ANON_STORAGE_KEY);
@@ -207,8 +226,30 @@ function rotateAnonymousId() {
207
226
  cachedAnonymousId = id;
208
227
  safeWriteLocalStorage(ANON_STORAGE_KEY, id);
209
228
  lastIdentifyFingerprint = null;
229
+ currentUserId = null;
230
+ return id;
231
+ }
232
+ function getOrMintSdkSessionId() {
233
+ if (cachedSdkSessionId) return cachedSdkSessionId;
234
+ const existing = safeReadSessionStorage(SDK_SESSION_STORAGE_KEY);
235
+ if (existing && /^[a-z0-9-]{8,}$/i.test(existing)) {
236
+ cachedSdkSessionId = existing;
237
+ return existing;
238
+ }
239
+ const id = generateRandomId();
240
+ safeWriteSessionStorage(SDK_SESSION_STORAGE_KEY, id);
241
+ cachedSdkSessionId = id;
210
242
  return id;
211
243
  }
244
+ function getCurrentUserId() {
245
+ return currentUserId;
246
+ }
247
+ function publishReplayStartMs(epochMs) {
248
+ if (replayStartMs === null) replayStartMs = epochMs;
249
+ }
250
+ function getReplayStartMs() {
251
+ return replayStartMs;
252
+ }
212
253
  function fingerprintUser(anonymousId, user) {
213
254
  const traits = user.traits ?? {};
214
255
  const keys = Object.keys(traits).sort();
@@ -217,6 +258,7 @@ function fingerprintUser(anonymousId, user) {
217
258
  }
218
259
  async function identifyIfChanged(transport, user) {
219
260
  const anonymousId = getOrMintAnonymousId();
261
+ currentUserId = user.id;
220
262
  const fp = fingerprintUser(anonymousId, user);
221
263
  if (fp === lastIdentifyFingerprint) return false;
222
264
  const url = `${transport.apiUrl.replace(/\/$/, "")}/api/identify`;
@@ -905,7 +947,15 @@ function initUseroFeedbackWidget(props) {
905
947
  } else {
906
948
  resolveAndApplyGetUser();
907
949
  }
908
- }
950
+ },
951
+ // Core-owned cross-cutting identity. Every plugin reads the same
952
+ // source of truth in identity.ts, so user-test and session-replay
953
+ // agree on the per-tab sdkSessionId without importing each other.
954
+ getSdkSessionId: () => getOrMintSdkSessionId(),
955
+ getAnonymousId: () => getOrMintAnonymousId(),
956
+ getUserId: () => getCurrentUserId(),
957
+ getReplayStartMs: () => getReplayStartMs(),
958
+ publishReplayStartMs: (epochMs) => publishReplayStartMs(epochMs)
909
959
  };
910
960
  pluginContexts.set(plugin.name, ctx);
911
961
  if (plugin.onInit) {
@@ -987,21 +1037,22 @@ function initUseroFeedbackWidget(props) {
987
1037
  screenshotError = null;
988
1038
  if (!file.type.startsWith("image/")) {
989
1039
  screenshotError = "Image files only";
990
- render();
1040
+ updateUploadExtras();
991
1041
  return;
992
1042
  }
993
1043
  if (file.size > MAX_SCREENSHOT_BYTES) {
994
1044
  screenshotError = "Max 10MB";
995
- render();
1045
+ updateUploadExtras();
996
1046
  return;
997
1047
  }
998
1048
  if (screenshots.length >= MAX_SCREENSHOTS) {
999
1049
  screenshotError = `Max ${MAX_SCREENSHOTS} screenshots`;
1000
- render();
1050
+ updateUploadExtras();
1001
1051
  return;
1002
1052
  }
1003
1053
  isUploadingScreenshot = true;
1004
- render();
1054
+ updateUploadButton();
1055
+ updateUploadExtras();
1005
1056
  try {
1006
1057
  const uploaded = await apiClient.uploadScreenshot(file, clientId);
1007
1058
  screenshots = [...screenshots, uploaded];
@@ -1009,12 +1060,14 @@ function initUseroFeedbackWidget(props) {
1009
1060
  screenshotError = err instanceof Error ? err.message : "Upload failed";
1010
1061
  } finally {
1011
1062
  isUploadingScreenshot = false;
1012
- render();
1063
+ updateUploadButton();
1064
+ updateUploadExtras();
1013
1065
  }
1014
1066
  }
1015
1067
  function removeScreenshot(index) {
1016
1068
  screenshots = screenshots.filter((_, i) => i !== index);
1017
- render();
1069
+ updateUploadButton();
1070
+ updateUploadExtras();
1018
1071
  }
1019
1072
  function close() {
1020
1073
  if (!isOpen) return;
@@ -1022,6 +1075,57 @@ function initUseroFeedbackWidget(props) {
1022
1075
  onClose?.();
1023
1076
  render();
1024
1077
  }
1078
+ function buildUploadButtonInner() {
1079
+ return isUploadingScreenshot ? '<span class="fb-ups"></span> Uploading...' : "\u{1F4F7} Add screenshot";
1080
+ }
1081
+ function buildUploadButtonHtml() {
1082
+ const atMax = screenshots.length >= MAX_SCREENSHOTS;
1083
+ const btnDisabled = isUploadingScreenshot || atMax;
1084
+ return `
1085
+ <input type="file" accept="image/*" data-role="screenshot-input" style="display:none;" aria-label="Choose screenshot" />
1086
+ <button type="button" class="fb-upb ${btnDisabled ? "fb-upb--dis" : ""}" data-role="screenshot-pick" ${btnDisabled ? "disabled" : ""} style="border:1px solid ${theme.border};color:${theme.text};">
1087
+ ${buildUploadButtonInner()}
1088
+ </button>
1089
+ `;
1090
+ }
1091
+ function buildUploadExtrasHtml() {
1092
+ const atMax = screenshots.length >= MAX_SCREENSHOTS;
1093
+ const previewsHtml = screenshots.map(
1094
+ (shot, i) => `
1095
+ <div class="fb-sp">
1096
+ <img src="${escapeHtml(shot.url)}" alt="Screenshot ${i + 1}" class="fb-si" />
1097
+ <button type="button" class="fb-sr" data-role="screenshot-remove" data-index="${i}" aria-label="Remove screenshot">\u2715</button>
1098
+ </div>
1099
+ `
1100
+ ).join("");
1101
+ const errorHtml = screenshotError ? `<div class="fb-upe">\u26A0 ${escapeHtml(screenshotError)}</div>` : "";
1102
+ const limitHtml = atMax ? `<div class="fb-sl">Max ${MAX_SCREENSHOTS}</div>` : "";
1103
+ return screenshotError || screenshots.length > 0 || atMax ? `<div class="fb-up-extras">${errorHtml}${screenshots.length > 0 ? `<div class="fb-ss">${previewsHtml}</div>` : ""}${limitHtml}</div>` : "";
1104
+ }
1105
+ function updateUploadButton() {
1106
+ if (!showScreenshotOption) return;
1107
+ const pickBtn = panelEl.querySelector(
1108
+ 'button[data-role="screenshot-pick"]'
1109
+ );
1110
+ if (!pickBtn) return;
1111
+ const atMax = screenshots.length >= MAX_SCREENSHOTS;
1112
+ const btnDisabled = isUploadingScreenshot || atMax;
1113
+ pickBtn.disabled = btnDisabled;
1114
+ pickBtn.classList.toggle("fb-upb--dis", btnDisabled);
1115
+ pickBtn.innerHTML = buildUploadButtonInner();
1116
+ }
1117
+ function updateUploadExtras() {
1118
+ if (!showScreenshotOption) return;
1119
+ const container = panelEl.querySelector(".fb-up");
1120
+ if (!container) return;
1121
+ container.innerHTML = buildUploadExtrasHtml();
1122
+ container.querySelectorAll('button[data-role="screenshot-remove"]').forEach((btn) => {
1123
+ btn.addEventListener("click", () => {
1124
+ const idx = Number(btn.dataset.index);
1125
+ if (Number.isInteger(idx)) removeScreenshot(idx);
1126
+ });
1127
+ });
1128
+ }
1025
1129
  async function submitForm() {
1026
1130
  if (isSubmitting) return;
1027
1131
  isSubmitting = true;
@@ -1139,30 +1243,8 @@ function initUseroFeedbackWidget(props) {
1139
1243
  `;
1140
1244
  }).join("");
1141
1245
  const messageHtml = submitMessage ? `<div class="fb-msg fb-msg--header ${submitMessage.type === "success" ? "fb-msg--ok" : "fb-msg--err"}">${submitMessage.type === "success" ? "\u2713" : "\u26A0"} ${escapeHtml(submitMessage.text)}</div>` : "";
1142
- const uploadBtnHtml = showScreenshotOption ? (() => {
1143
- const atMax = screenshots.length >= MAX_SCREENSHOTS;
1144
- const btnDisabled = isUploadingScreenshot || atMax;
1145
- return `
1146
- <input type="file" accept="image/*" data-role="screenshot-input" style="display:none;" aria-label="Choose screenshot" />
1147
- <button type="button" class="fb-upb ${btnDisabled ? "fb-upb--dis" : ""}" data-role="screenshot-pick" ${btnDisabled ? "disabled" : ""} style="border:1px solid ${theme.border};color:${theme.text};">
1148
- ${isUploadingScreenshot ? '<span class="fb-ups"></span> Uploading...' : "\u{1F4F7} Add screenshot"}
1149
- </button>
1150
- `;
1151
- })() : "";
1152
- const uploadExtrasHtml = showScreenshotOption ? (() => {
1153
- const atMax = screenshots.length >= MAX_SCREENSHOTS;
1154
- const previewsHtml = screenshots.map(
1155
- (shot, i) => `
1156
- <div class="fb-sp">
1157
- <img src="${escapeHtml(shot.url)}" alt="Screenshot ${i + 1}" class="fb-si" />
1158
- <button type="button" class="fb-sr" data-role="screenshot-remove" data-index="${i}" aria-label="Remove screenshot">\u2715</button>
1159
- </div>
1160
- `
1161
- ).join("");
1162
- const errorHtml = screenshotError ? `<div class="fb-upe">\u26A0 ${escapeHtml(screenshotError)}</div>` : "";
1163
- const limitHtml = atMax ? `<div class="fb-sl">Max ${MAX_SCREENSHOTS}</div>` : "";
1164
- return screenshotError || screenshots.length > 0 || atMax ? `<div class="fb-up-extras">${errorHtml}${screenshots.length > 0 ? `<div class="fb-ss">${previewsHtml}</div>` : ""}${limitHtml}</div>` : "";
1165
- })() : "";
1246
+ const uploadBtnHtml = showScreenshotOption ? buildUploadButtonHtml() : "";
1247
+ const uploadExtrasHtml = showScreenshotOption ? buildUploadExtrasHtml() : "";
1166
1248
  const emailBlockHtml = showEmailOption ? `
1167
1249
  <div class="fb-email">
1168
1250
  <label class="fb-email-lbl" style="color:${theme.text}">
@@ -1188,7 +1270,7 @@ function initUseroFeedbackWidget(props) {
1188
1270
  ${uploadBtnHtml}
1189
1271
  <div class="fb-charcount${lowChars ? " fb-charcount--low" : ""}" data-role="charcount" style="color:${lowChars ? "#dc2626" : theme.text};opacity:${lowChars ? 1 : 0.6};">${remaining} chars remaining</div>
1190
1272
  </div>
1191
- ${uploadExtrasHtml ? `<div class="fb-up">${uploadExtrasHtml}</div>` : ""}
1273
+ ${showScreenshotOption ? `<div class="fb-up">${uploadExtrasHtml}</div>` : ""}
1192
1274
  ${emailBlockHtml}
1193
1275
  <button class="fb-sub ${submitDisabled ? "fb-sub--dis" : ""}" type="submit" aria-label="Submit" ${submitDisabled ? "disabled" : ""} style="${submitStyle}">
1194
1276
  ${isSubmitting ? '<span class="fb-spin"></span>' : ""}
@@ -1285,10 +1367,16 @@ function initUseroFeedbackWidget(props) {
1285
1367
  if (isOpen) close();
1286
1368
  else open();
1287
1369
  });
1288
- backdropEl.addEventListener("click", close);
1370
+ backdropEl.addEventListener("click", () => {
1371
+ if (isUploadingScreenshot || isSubmitting) return;
1372
+ close();
1373
+ });
1289
1374
  const onKeyDown = (e) => {
1290
1375
  if (!isOpen) return;
1291
- if (e.key === "Escape") close();
1376
+ if (e.key === "Escape") {
1377
+ if (isUploadingScreenshot || isSubmitting) return;
1378
+ close();
1379
+ }
1292
1380
  if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
1293
1381
  e.preventDefault();
1294
1382
  void submitForm();