@usero/sdk 0.3.4 → 0.4.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/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,9 @@ 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
+ }
781
942
  function notifyShadowUpdate(reason) {
782
943
  try {
783
944
  window.dispatchEvent(
@@ -813,6 +974,7 @@ function initUseroFeedbackWidget(props) {
813
974
  screenshotError = null;
814
975
  isUploadingScreenshot = false;
815
976
  apiClient.ping();
977
+ pollGetUser();
816
978
  onOpen?.();
817
979
  render();
818
980
  notifyShadowUpdate("panel-open");
@@ -1170,6 +1332,11 @@ function initUseroFeedbackWidget(props) {
1170
1332
  open,
1171
1333
  close,
1172
1334
  whenReady: () => readyPromise,
1335
+ identify: (user) => {
1336
+ if (destroyed) return;
1337
+ currentUserProp = user;
1338
+ applyResolvedUser(user);
1339
+ },
1173
1340
  update: (next) => {
1174
1341
  if (destroyed) return;
1175
1342
  let needsRender = false;
@@ -1206,6 +1373,11 @@ function initUseroFeedbackWidget(props) {
1206
1373
  if ("onError" in next) onError = next.onError;
1207
1374
  if ("onOpen" in next) onOpen = next.onOpen;
1208
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
+ }
1209
1381
  if (needsRender) render();
1210
1382
  }
1211
1383
  };