@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/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
|
};
|