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