@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/plugins/session-replay.cjs +101 -9
- package/dist/plugins/session-replay.cjs.map +1 -1
- package/dist/plugins/session-replay.d.cts +67 -1
- package/dist/plugins/session-replay.d.ts +67 -1
- package/dist/plugins/session-replay.js +101 -9
- package/dist/plugins/session-replay.js.map +1 -1
- package/dist/plugins/user-test.cjs.map +1 -1
- package/dist/plugins/user-test.d.cts +1 -0
- package/dist/plugins/user-test.d.ts +1 -0
- package/dist/plugins/user-test.js.map +1 -1
- package/dist/react.cjs +184 -2
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +13 -1
- package/dist/react.d.ts +13 -1
- package/dist/react.js +184 -2
- package/dist/react.js.map +1 -1
- package/dist/usero.iife.js +27 -27
- package/dist/usero.iife.js.map +1 -1
- package/dist/vanilla.cjs +174 -2
- package/dist/vanilla.cjs.map +1 -1
- package/dist/vanilla.d.cts +12 -0
- package/dist/vanilla.d.ts +12 -0
- package/dist/vanilla.js +174 -2
- package/dist/vanilla.js.map +1 -1
- package/package.json +1 -1
package/dist/react.cjs
CHANGED
|
@@ -161,6 +161,111 @@ function getGradientEnd(color) {
|
|
|
161
161
|
return `#${[shiftedR, shiftedG, shiftedB].map((x) => x.toString(16).padStart(2, "0")).join("")}`;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
// src/identity.ts
|
|
165
|
+
var ANON_STORAGE_KEY = "usero:anonymous-id";
|
|
166
|
+
var cachedAnonymousId = null;
|
|
167
|
+
var lastIdentifyFingerprint = null;
|
|
168
|
+
function generateRandomId() {
|
|
169
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
170
|
+
return crypto.randomUUID();
|
|
171
|
+
}
|
|
172
|
+
const bytes = new Uint8Array(16);
|
|
173
|
+
if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
|
|
174
|
+
crypto.getRandomValues(bytes);
|
|
175
|
+
} else {
|
|
176
|
+
for (let i = 0; i < bytes.length; i += 1) bytes[i] = Math.floor(Math.random() * 256);
|
|
177
|
+
}
|
|
178
|
+
let out = "";
|
|
179
|
+
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
function safeReadLocalStorage(key) {
|
|
183
|
+
if (typeof window === "undefined") return null;
|
|
184
|
+
try {
|
|
185
|
+
return window.localStorage?.getItem(key) ?? null;
|
|
186
|
+
} catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function safeWriteLocalStorage(key, value) {
|
|
191
|
+
if (typeof window === "undefined") return;
|
|
192
|
+
try {
|
|
193
|
+
window.localStorage?.setItem(key, value);
|
|
194
|
+
} catch {
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function getOrMintAnonymousId() {
|
|
198
|
+
if (cachedAnonymousId) return cachedAnonymousId;
|
|
199
|
+
const existing = safeReadLocalStorage(ANON_STORAGE_KEY);
|
|
200
|
+
if (existing && /^[a-z0-9-]{8,}$/i.test(existing)) {
|
|
201
|
+
cachedAnonymousId = existing;
|
|
202
|
+
return existing;
|
|
203
|
+
}
|
|
204
|
+
const id = generateRandomId();
|
|
205
|
+
safeWriteLocalStorage(ANON_STORAGE_KEY, id);
|
|
206
|
+
cachedAnonymousId = id;
|
|
207
|
+
return id;
|
|
208
|
+
}
|
|
209
|
+
function rotateAnonymousId() {
|
|
210
|
+
const id = generateRandomId();
|
|
211
|
+
cachedAnonymousId = id;
|
|
212
|
+
safeWriteLocalStorage(ANON_STORAGE_KEY, id);
|
|
213
|
+
lastIdentifyFingerprint = null;
|
|
214
|
+
return id;
|
|
215
|
+
}
|
|
216
|
+
function fingerprintUser(anonymousId, user) {
|
|
217
|
+
const traits = user.traits ?? {};
|
|
218
|
+
const keys = Object.keys(traits).sort();
|
|
219
|
+
const canonical = keys.map((k) => [k, traits[k] ?? null]);
|
|
220
|
+
return JSON.stringify([anonymousId, user.id, user.email ?? null, user.displayName ?? null, canonical]);
|
|
221
|
+
}
|
|
222
|
+
async function identifyIfChanged(transport, user) {
|
|
223
|
+
const anonymousId = getOrMintAnonymousId();
|
|
224
|
+
const fp = fingerprintUser(anonymousId, user);
|
|
225
|
+
if (fp === lastIdentifyFingerprint) return false;
|
|
226
|
+
const url = `${transport.apiUrl.replace(/\/$/, "")}/api/identify`;
|
|
227
|
+
const body = JSON.stringify({
|
|
228
|
+
clientId: transport.clientId,
|
|
229
|
+
anonymousId,
|
|
230
|
+
externalUserId: user.id,
|
|
231
|
+
email: user.email,
|
|
232
|
+
displayName: user.displayName,
|
|
233
|
+
traits: user.traits
|
|
234
|
+
});
|
|
235
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden" && typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
|
|
236
|
+
try {
|
|
237
|
+
const blob = new Blob([body], { type: "application/json" });
|
|
238
|
+
if (navigator.sendBeacon(url, blob)) {
|
|
239
|
+
lastIdentifyFingerprint = fp;
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
} catch {
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
const res = await fetch(url, {
|
|
247
|
+
method: "POST",
|
|
248
|
+
headers: { "Content-Type": "application/json" },
|
|
249
|
+
body,
|
|
250
|
+
// keepalive lets the request survive a tab-close mid-flight on
|
|
251
|
+
// browsers that support it; sendBeacon above is the primary path.
|
|
252
|
+
keepalive: true
|
|
253
|
+
});
|
|
254
|
+
if (!res.ok) return true;
|
|
255
|
+
try {
|
|
256
|
+
const json = await res.json();
|
|
257
|
+
if (json && json.accepted === true) lastIdentifyFingerprint = fp;
|
|
258
|
+
} catch {
|
|
259
|
+
}
|
|
260
|
+
return true;
|
|
261
|
+
} catch {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function handleLogout() {
|
|
266
|
+
rotateAnonymousId();
|
|
267
|
+
}
|
|
268
|
+
|
|
164
269
|
// src/plugin.ts
|
|
165
270
|
function createPluginLogger(name) {
|
|
166
271
|
const prefix = `[usero:${name}]`;
|
|
@@ -706,7 +811,9 @@ function initUseroFeedbackWidget(props) {
|
|
|
706
811
|
},
|
|
707
812
|
update: () => {
|
|
708
813
|
},
|
|
709
|
-
whenReady: () => Promise.resolve()
|
|
814
|
+
whenReady: () => Promise.resolve(),
|
|
815
|
+
identify: () => {
|
|
816
|
+
}
|
|
710
817
|
};
|
|
711
818
|
}
|
|
712
819
|
const { clientId, baseUrl } = props;
|
|
@@ -722,7 +829,9 @@ function initUseroFeedbackWidget(props) {
|
|
|
722
829
|
},
|
|
723
830
|
update: () => {
|
|
724
831
|
},
|
|
725
|
-
whenReady: () => Promise.resolve()
|
|
832
|
+
whenReady: () => Promise.resolve(),
|
|
833
|
+
identify: () => {
|
|
834
|
+
}
|
|
726
835
|
};
|
|
727
836
|
}
|
|
728
837
|
let position = props.position ?? "right";
|
|
@@ -738,7 +847,44 @@ function initUseroFeedbackWidget(props) {
|
|
|
738
847
|
let onError = props.onError;
|
|
739
848
|
let onOpen = props.onOpen;
|
|
740
849
|
let onClose = props.onClose;
|
|
850
|
+
let getUserFn = props.getUser;
|
|
851
|
+
let currentUserProp = props.user;
|
|
741
852
|
const apiClient = new FeedbackApiClient(baseUrl);
|
|
853
|
+
const identifyTransport = { apiUrl: baseUrl ?? DEFAULT_API_URL, clientId };
|
|
854
|
+
let lastUserId = null;
|
|
855
|
+
let lastTraitsRef;
|
|
856
|
+
let lastEmail;
|
|
857
|
+
let lastDisplayName;
|
|
858
|
+
function applyResolvedUser(user) {
|
|
859
|
+
const next = user ?? null;
|
|
860
|
+
if (next) {
|
|
861
|
+
const unchanged = next.id === lastUserId && next.traits === lastTraitsRef && next.email === lastEmail && next.displayName === lastDisplayName;
|
|
862
|
+
if (unchanged) return;
|
|
863
|
+
void identifyIfChanged(identifyTransport, next);
|
|
864
|
+
lastUserId = next.id;
|
|
865
|
+
lastTraitsRef = next.traits;
|
|
866
|
+
lastEmail = next.email;
|
|
867
|
+
lastDisplayName = next.displayName;
|
|
868
|
+
} else if (lastUserId !== null) {
|
|
869
|
+
handleLogout();
|
|
870
|
+
lastUserId = null;
|
|
871
|
+
lastTraitsRef = void 0;
|
|
872
|
+
lastEmail = void 0;
|
|
873
|
+
lastDisplayName = void 0;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
function resolveAndApplyGetUser() {
|
|
877
|
+
if (!getUserFn) return;
|
|
878
|
+
try {
|
|
879
|
+
applyResolvedUser(getUserFn() ?? null);
|
|
880
|
+
} catch {
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
if (props.user !== void 0) {
|
|
884
|
+
applyResolvedUser(props.user);
|
|
885
|
+
} else if (getUserFn) {
|
|
886
|
+
resolveAndApplyGetUser();
|
|
887
|
+
}
|
|
742
888
|
const pluginList = props.plugins ?? [];
|
|
743
889
|
const pluginStores = /* @__PURE__ */ new Map();
|
|
744
890
|
const pluginContexts = /* @__PURE__ */ new Map();
|
|
@@ -751,6 +897,18 @@ function initUseroFeedbackWidget(props) {
|
|
|
751
897
|
getStore: () => pluginStores.get(plugin.name),
|
|
752
898
|
setStore: (value) => {
|
|
753
899
|
pluginStores.set(plugin.name, value);
|
|
900
|
+
},
|
|
901
|
+
// Expose the same user-resolution path the widget uses, so plugins
|
|
902
|
+
// (e.g. session-replay for replay-only installs that never open the
|
|
903
|
+
// widget) can re-poll user state at their own boundaries. Prefers
|
|
904
|
+
// the imperative `user` prop when set, falls back to `getUser`.
|
|
905
|
+
resolveUser: () => {
|
|
906
|
+
if (destroyed) return;
|
|
907
|
+
if (currentUserProp !== void 0) {
|
|
908
|
+
applyResolvedUser(currentUserProp);
|
|
909
|
+
} else {
|
|
910
|
+
resolveAndApplyGetUser();
|
|
911
|
+
}
|
|
754
912
|
}
|
|
755
913
|
};
|
|
756
914
|
pluginContexts.set(plugin.name, ctx);
|
|
@@ -784,6 +942,9 @@ function initUseroFeedbackWidget(props) {
|
|
|
784
942
|
host.style.cssText = "all: initial;";
|
|
785
943
|
document.body.appendChild(host);
|
|
786
944
|
const root = host.attachShadow({ mode: "open" });
|
|
945
|
+
function pollGetUser() {
|
|
946
|
+
resolveAndApplyGetUser();
|
|
947
|
+
}
|
|
787
948
|
function notifyShadowUpdate(reason) {
|
|
788
949
|
try {
|
|
789
950
|
window.dispatchEvent(
|
|
@@ -819,6 +980,7 @@ function initUseroFeedbackWidget(props) {
|
|
|
819
980
|
screenshotError = null;
|
|
820
981
|
isUploadingScreenshot = false;
|
|
821
982
|
apiClient.ping();
|
|
983
|
+
pollGetUser();
|
|
822
984
|
onOpen?.();
|
|
823
985
|
render();
|
|
824
986
|
notifyShadowUpdate("panel-open");
|
|
@@ -1176,6 +1338,11 @@ function initUseroFeedbackWidget(props) {
|
|
|
1176
1338
|
open,
|
|
1177
1339
|
close,
|
|
1178
1340
|
whenReady: () => readyPromise,
|
|
1341
|
+
identify: (user) => {
|
|
1342
|
+
if (destroyed) return;
|
|
1343
|
+
currentUserProp = user;
|
|
1344
|
+
applyResolvedUser(user);
|
|
1345
|
+
},
|
|
1179
1346
|
update: (next) => {
|
|
1180
1347
|
if (destroyed) return;
|
|
1181
1348
|
let needsRender = false;
|
|
@@ -1212,6 +1379,11 @@ function initUseroFeedbackWidget(props) {
|
|
|
1212
1379
|
if ("onError" in next) onError = next.onError;
|
|
1213
1380
|
if ("onOpen" in next) onOpen = next.onOpen;
|
|
1214
1381
|
if ("onClose" in next) onClose = next.onClose;
|
|
1382
|
+
if ("getUser" in next) getUserFn = next.getUser;
|
|
1383
|
+
if ("user" in next) {
|
|
1384
|
+
currentUserProp = next.user;
|
|
1385
|
+
applyResolvedUser(next.user);
|
|
1386
|
+
}
|
|
1215
1387
|
if (needsRender) render();
|
|
1216
1388
|
}
|
|
1217
1389
|
};
|
|
@@ -1276,6 +1448,16 @@ function UseroFeedbackWidget(props) {
|
|
|
1276
1448
|
props.environment,
|
|
1277
1449
|
metadataJson
|
|
1278
1450
|
]);
|
|
1451
|
+
const userId = props.user?.id ?? null;
|
|
1452
|
+
const userEmail = props.user?.email ?? null;
|
|
1453
|
+
const userDisplayName = props.user?.displayName ?? null;
|
|
1454
|
+
const userTraitsJson = JSON.stringify(props.user?.traits ?? null);
|
|
1455
|
+
const userIsNull = props.user === null;
|
|
1456
|
+
react.useEffect(() => {
|
|
1457
|
+
const handle = handleRef.current;
|
|
1458
|
+
if (!handle) return;
|
|
1459
|
+
if (props.user !== void 0) handle.update({ user: props.user });
|
|
1460
|
+
}, [userId, userEmail, userDisplayName, userTraitsJson, userIsNull]);
|
|
1279
1461
|
return null;
|
|
1280
1462
|
}
|
|
1281
1463
|
|