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