@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.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,20 @@ 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
|
+
}
|
|
944
|
+
function notifyShadowUpdate(reason) {
|
|
945
|
+
try {
|
|
946
|
+
window.dispatchEvent(
|
|
947
|
+
new CustomEvent("usero:shadow-update", {
|
|
948
|
+
detail: { host, root, reason }
|
|
949
|
+
})
|
|
950
|
+
);
|
|
951
|
+
} catch {
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
notifyShadowUpdate("mount");
|
|
783
955
|
const style = document.createElement("style");
|
|
784
956
|
style.textContent = FEEDBACK_CSS;
|
|
785
957
|
root.appendChild(style);
|
|
@@ -804,8 +976,10 @@ function initUseroFeedbackWidget(props) {
|
|
|
804
976
|
screenshotError = null;
|
|
805
977
|
isUploadingScreenshot = false;
|
|
806
978
|
apiClient.ping();
|
|
979
|
+
pollGetUser();
|
|
807
980
|
onOpen?.();
|
|
808
981
|
render();
|
|
982
|
+
notifyShadowUpdate("panel-open");
|
|
809
983
|
}
|
|
810
984
|
async function handleScreenshotFile(file) {
|
|
811
985
|
screenshotError = null;
|
|
@@ -1160,6 +1334,11 @@ function initUseroFeedbackWidget(props) {
|
|
|
1160
1334
|
open,
|
|
1161
1335
|
close,
|
|
1162
1336
|
whenReady: () => readyPromise,
|
|
1337
|
+
identify: (user) => {
|
|
1338
|
+
if (destroyed) return;
|
|
1339
|
+
currentUserProp = user;
|
|
1340
|
+
applyResolvedUser(user);
|
|
1341
|
+
},
|
|
1163
1342
|
update: (next) => {
|
|
1164
1343
|
if (destroyed) return;
|
|
1165
1344
|
let needsRender = false;
|
|
@@ -1196,6 +1375,11 @@ function initUseroFeedbackWidget(props) {
|
|
|
1196
1375
|
if ("onError" in next) onError = next.onError;
|
|
1197
1376
|
if ("onOpen" in next) onOpen = next.onOpen;
|
|
1198
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
|
+
}
|
|
1199
1383
|
if (needsRender) render();
|
|
1200
1384
|
}
|
|
1201
1385
|
};
|