@usero/sdk 0.3.2 → 0.4.0

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
@@ -155,6 +155,111 @@ function getGradientEnd(color) {
155
155
  return `#${[shiftedR, shiftedG, shiftedB].map((x) => x.toString(16).padStart(2, "0")).join("")}`;
156
156
  }
157
157
 
158
+ // src/identity.ts
159
+ var ANON_STORAGE_KEY = "usero:anonymous-id";
160
+ var cachedAnonymousId = null;
161
+ var lastIdentifyFingerprint = null;
162
+ function generateRandomId() {
163
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
164
+ return crypto.randomUUID();
165
+ }
166
+ const bytes = new Uint8Array(16);
167
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
168
+ crypto.getRandomValues(bytes);
169
+ } else {
170
+ for (let i = 0; i < bytes.length; i += 1) bytes[i] = Math.floor(Math.random() * 256);
171
+ }
172
+ let out = "";
173
+ for (const b of bytes) out += b.toString(16).padStart(2, "0");
174
+ return out;
175
+ }
176
+ function safeReadLocalStorage(key) {
177
+ if (typeof window === "undefined") return null;
178
+ try {
179
+ return window.localStorage?.getItem(key) ?? null;
180
+ } catch {
181
+ return null;
182
+ }
183
+ }
184
+ function safeWriteLocalStorage(key, value) {
185
+ if (typeof window === "undefined") return;
186
+ try {
187
+ window.localStorage?.setItem(key, value);
188
+ } catch {
189
+ }
190
+ }
191
+ function getOrMintAnonymousId() {
192
+ if (cachedAnonymousId) return cachedAnonymousId;
193
+ const existing = safeReadLocalStorage(ANON_STORAGE_KEY);
194
+ if (existing && /^[a-z0-9-]{8,}$/i.test(existing)) {
195
+ cachedAnonymousId = existing;
196
+ return existing;
197
+ }
198
+ const id = generateRandomId();
199
+ safeWriteLocalStorage(ANON_STORAGE_KEY, id);
200
+ cachedAnonymousId = id;
201
+ return id;
202
+ }
203
+ function rotateAnonymousId() {
204
+ const id = generateRandomId();
205
+ cachedAnonymousId = id;
206
+ safeWriteLocalStorage(ANON_STORAGE_KEY, id);
207
+ lastIdentifyFingerprint = null;
208
+ return id;
209
+ }
210
+ function fingerprintUser(anonymousId, user) {
211
+ const traits = user.traits ?? {};
212
+ const keys = Object.keys(traits).sort();
213
+ const canonical = keys.map((k) => [k, traits[k] ?? null]);
214
+ return JSON.stringify([anonymousId, user.id, user.email ?? null, user.displayName ?? null, canonical]);
215
+ }
216
+ async function identifyIfChanged(transport, user) {
217
+ const anonymousId = getOrMintAnonymousId();
218
+ const fp = fingerprintUser(anonymousId, user);
219
+ if (fp === lastIdentifyFingerprint) return false;
220
+ const url = `${transport.apiUrl.replace(/\/$/, "")}/api/identify`;
221
+ const body = JSON.stringify({
222
+ clientId: transport.clientId,
223
+ anonymousId,
224
+ externalUserId: user.id,
225
+ email: user.email,
226
+ displayName: user.displayName,
227
+ traits: user.traits
228
+ });
229
+ if (typeof document !== "undefined" && document.visibilityState === "hidden" && typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
230
+ try {
231
+ const blob = new Blob([body], { type: "application/json" });
232
+ if (navigator.sendBeacon(url, blob)) {
233
+ lastIdentifyFingerprint = fp;
234
+ return true;
235
+ }
236
+ } catch {
237
+ }
238
+ }
239
+ try {
240
+ const res = await fetch(url, {
241
+ method: "POST",
242
+ headers: { "Content-Type": "application/json" },
243
+ body,
244
+ // keepalive lets the request survive a tab-close mid-flight on
245
+ // browsers that support it; sendBeacon above is the primary path.
246
+ keepalive: true
247
+ });
248
+ if (!res.ok) return true;
249
+ try {
250
+ const json = await res.json();
251
+ if (json && json.accepted === true) lastIdentifyFingerprint = fp;
252
+ } catch {
253
+ }
254
+ return true;
255
+ } catch {
256
+ return false;
257
+ }
258
+ }
259
+ function handleLogout() {
260
+ rotateAnonymousId();
261
+ }
262
+
158
263
  // src/plugin.ts
159
264
  function createPluginLogger(name) {
160
265
  const prefix = `[usero:${name}]`;
@@ -700,7 +805,9 @@ function initUseroFeedbackWidget(props) {
700
805
  },
701
806
  update: () => {
702
807
  },
703
- whenReady: () => Promise.resolve()
808
+ whenReady: () => Promise.resolve(),
809
+ identify: () => {
810
+ }
704
811
  };
705
812
  }
706
813
  const { clientId, baseUrl } = props;
@@ -716,7 +823,9 @@ function initUseroFeedbackWidget(props) {
716
823
  },
717
824
  update: () => {
718
825
  },
719
- whenReady: () => Promise.resolve()
826
+ whenReady: () => Promise.resolve(),
827
+ identify: () => {
828
+ }
720
829
  };
721
830
  }
722
831
  let position = props.position ?? "right";
@@ -732,7 +841,44 @@ function initUseroFeedbackWidget(props) {
732
841
  let onError = props.onError;
733
842
  let onOpen = props.onOpen;
734
843
  let onClose = props.onClose;
844
+ let getUserFn = props.getUser;
845
+ let currentUserProp = props.user;
735
846
  const apiClient = new FeedbackApiClient(baseUrl);
847
+ const identifyTransport = { apiUrl: baseUrl ?? DEFAULT_API_URL, clientId };
848
+ let lastUserId = null;
849
+ let lastTraitsRef;
850
+ let lastEmail;
851
+ let lastDisplayName;
852
+ function applyResolvedUser(user) {
853
+ const next = user ?? null;
854
+ if (next) {
855
+ const unchanged = next.id === lastUserId && next.traits === lastTraitsRef && next.email === lastEmail && next.displayName === lastDisplayName;
856
+ if (unchanged) return;
857
+ void identifyIfChanged(identifyTransport, next);
858
+ lastUserId = next.id;
859
+ lastTraitsRef = next.traits;
860
+ lastEmail = next.email;
861
+ lastDisplayName = next.displayName;
862
+ } else if (lastUserId !== null) {
863
+ handleLogout();
864
+ lastUserId = null;
865
+ lastTraitsRef = void 0;
866
+ lastEmail = void 0;
867
+ lastDisplayName = void 0;
868
+ }
869
+ }
870
+ function resolveAndApplyGetUser() {
871
+ if (!getUserFn) return;
872
+ try {
873
+ applyResolvedUser(getUserFn() ?? null);
874
+ } catch {
875
+ }
876
+ }
877
+ if (props.user !== void 0) {
878
+ applyResolvedUser(props.user);
879
+ } else if (getUserFn) {
880
+ resolveAndApplyGetUser();
881
+ }
736
882
  const pluginList = props.plugins ?? [];
737
883
  const pluginStores = /* @__PURE__ */ new Map();
738
884
  const pluginContexts = /* @__PURE__ */ new Map();
@@ -745,6 +891,18 @@ function initUseroFeedbackWidget(props) {
745
891
  getStore: () => pluginStores.get(plugin.name),
746
892
  setStore: (value) => {
747
893
  pluginStores.set(plugin.name, value);
894
+ },
895
+ // Expose the same user-resolution path the widget uses, so plugins
896
+ // (e.g. session-replay for replay-only installs that never open the
897
+ // widget) can re-poll user state at their own boundaries. Prefers
898
+ // the imperative `user` prop when set, falls back to `getUser`.
899
+ resolveUser: () => {
900
+ if (destroyed) return;
901
+ if (currentUserProp !== void 0) {
902
+ applyResolvedUser(currentUserProp);
903
+ } else {
904
+ resolveAndApplyGetUser();
905
+ }
748
906
  }
749
907
  };
750
908
  pluginContexts.set(plugin.name, ctx);
@@ -778,6 +936,20 @@ function initUseroFeedbackWidget(props) {
778
936
  host.style.cssText = "all: initial;";
779
937
  document.body.appendChild(host);
780
938
  const root = host.attachShadow({ mode: "open" });
939
+ function pollGetUser() {
940
+ resolveAndApplyGetUser();
941
+ }
942
+ function notifyShadowUpdate(reason) {
943
+ try {
944
+ window.dispatchEvent(
945
+ new CustomEvent("usero:shadow-update", {
946
+ detail: { host, root, reason }
947
+ })
948
+ );
949
+ } catch {
950
+ }
951
+ }
952
+ notifyShadowUpdate("mount");
781
953
  const style = document.createElement("style");
782
954
  style.textContent = FEEDBACK_CSS;
783
955
  root.appendChild(style);
@@ -802,8 +974,10 @@ function initUseroFeedbackWidget(props) {
802
974
  screenshotError = null;
803
975
  isUploadingScreenshot = false;
804
976
  apiClient.ping();
977
+ pollGetUser();
805
978
  onOpen?.();
806
979
  render();
980
+ notifyShadowUpdate("panel-open");
807
981
  }
808
982
  async function handleScreenshotFile(file) {
809
983
  screenshotError = null;
@@ -1158,6 +1332,11 @@ function initUseroFeedbackWidget(props) {
1158
1332
  open,
1159
1333
  close,
1160
1334
  whenReady: () => readyPromise,
1335
+ identify: (user) => {
1336
+ if (destroyed) return;
1337
+ currentUserProp = user;
1338
+ applyResolvedUser(user);
1339
+ },
1161
1340
  update: (next) => {
1162
1341
  if (destroyed) return;
1163
1342
  let needsRender = false;
@@ -1194,6 +1373,11 @@ function initUseroFeedbackWidget(props) {
1194
1373
  if ("onError" in next) onError = next.onError;
1195
1374
  if ("onOpen" in next) onOpen = next.onOpen;
1196
1375
  if ("onClose" in next) onClose = next.onClose;
1376
+ if ("getUser" in next) getUserFn = next.getUser;
1377
+ if ("user" in next) {
1378
+ currentUserProp = next.user;
1379
+ applyResolvedUser(next.user);
1380
+ }
1197
1381
  if (needsRender) render();
1198
1382
  }
1199
1383
  };