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