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