@palbase/web 1.7.0 → 1.9.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/{analytics-facade-9GPSKnVG.d.cts → analytics-facade-DFd5LB3_.d.cts} +190 -5
- package/dist/{analytics-facade-CQJ3b-zB.d.ts → analytics-facade-DsvKx5A5.d.ts} +190 -5
- package/dist/{chunk-QRO632M7.js → chunk-I3ZMB7XM.js} +632 -21
- package/dist/chunk-I3ZMB7XM.js.map +1 -0
- package/dist/index.cjs +99 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/internal.cjs +631 -20
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +3 -3
- package/dist/internal.d.ts +3 -3
- package/dist/internal.js +1 -1
- package/dist/next/client.cjs +621 -17
- package/dist/next/client.cjs.map +1 -1
- package/dist/next/client.js +1 -1
- package/dist/next/index.cjs +627 -17
- package/dist/next/index.cjs.map +1 -1
- package/dist/next/index.d.cts +2 -2
- package/dist/next/index.d.ts +2 -2
- package/dist/next/index.js +1 -1
- package/dist/{pb-BvfCJZux.d.ts → pb-CDVMy1l1.d.ts} +9 -1
- package/dist/{pb-D4HyUbpb.d.cts → pb-D_fuhafL.d.cts} +9 -1
- package/dist/react/index.cjs +69 -13
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-QRO632M7.js.map +0 -1
package/dist/internal.cjs
CHANGED
|
@@ -413,14 +413,47 @@ function unwrap(res) {
|
|
|
413
413
|
return res.data;
|
|
414
414
|
}
|
|
415
415
|
|
|
416
|
+
// src/perf/url-redactor.ts
|
|
417
|
+
var ID_SEGMENT = /^(?:\d+|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/;
|
|
418
|
+
var EMAIL_SEGMENT = /(?:@|%40)/i;
|
|
419
|
+
function isSensitiveSegment(seg2) {
|
|
420
|
+
return ID_SEGMENT.test(seg2) || EMAIL_SEGMENT.test(seg2);
|
|
421
|
+
}
|
|
422
|
+
function redactUrl(rawUrl) {
|
|
423
|
+
let path = rawUrl;
|
|
424
|
+
try {
|
|
425
|
+
path = new URL(rawUrl).pathname;
|
|
426
|
+
} catch {
|
|
427
|
+
const q = path.indexOf("?");
|
|
428
|
+
if (q >= 0) path = path.slice(0, q);
|
|
429
|
+
}
|
|
430
|
+
return path.split("/").map((seg2) => isSensitiveSegment(seg2) ? ":id" : seg2).join("/");
|
|
431
|
+
}
|
|
432
|
+
|
|
416
433
|
// src/request.ts
|
|
417
434
|
var MUTATING = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
435
|
+
var PERF_EXCLUDED_PREFIX = "/v1/analytics/";
|
|
436
|
+
function isSelfTraced(path) {
|
|
437
|
+
return path.startsWith(PERF_EXCLUDED_PREFIX);
|
|
438
|
+
}
|
|
439
|
+
function nowMs() {
|
|
440
|
+
return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
441
|
+
}
|
|
442
|
+
function isAbort(e) {
|
|
443
|
+
return e instanceof Error && e.name === "AbortError";
|
|
444
|
+
}
|
|
418
445
|
async function palbeRequest(rt, method, path, spec = {}) {
|
|
419
446
|
const headers = { ...spec.headers };
|
|
420
447
|
const callerHasKey = Object.keys(headers).some((k) => k.toLowerCase() === "idempotency-key");
|
|
421
448
|
if (MUTATING.has(method) && !callerHasKey) {
|
|
422
449
|
headers["Idempotency-Key"] = crypto.randomUUID();
|
|
423
450
|
}
|
|
451
|
+
if (rt.appIdentifier !== "") {
|
|
452
|
+
const callerHasBundle = Object.keys(headers).some(
|
|
453
|
+
(k) => k.toLowerCase() === "x-palbase-bundle"
|
|
454
|
+
);
|
|
455
|
+
if (!callerHasBundle) headers["X-Palbase-Bundle"] = rt.appIdentifier;
|
|
456
|
+
}
|
|
424
457
|
const attempt = async () => {
|
|
425
458
|
try {
|
|
426
459
|
return await rt.http.request(method, path, {
|
|
@@ -433,21 +466,38 @@ async function palbeRequest(rt, method, path, spec = {}) {
|
|
|
433
466
|
throw pe ? fromPalbaseError(pe) : e;
|
|
434
467
|
}
|
|
435
468
|
};
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
469
|
+
const traced = !isSelfTraced(path) && rt.perf !== void 0;
|
|
470
|
+
const startedAt = traced ? nowMs() : 0;
|
|
471
|
+
let recorded = false;
|
|
472
|
+
const record = (status) => {
|
|
473
|
+
if (!traced || recorded) return;
|
|
474
|
+
recorded = true;
|
|
475
|
+
rt.perf.recordNetwork(method, redactUrl(path), status, nowMs() - startedAt);
|
|
476
|
+
};
|
|
477
|
+
let res;
|
|
478
|
+
try {
|
|
479
|
+
res = await attempt();
|
|
480
|
+
if (res.error?.status === 401 && rt.tokenManager.getRefreshToken() && rt.tokenManager.refreshFunction) {
|
|
481
|
+
try {
|
|
482
|
+
await rt.tokenManager.refreshSession();
|
|
483
|
+
} catch (refreshErr) {
|
|
484
|
+
const pe = asPalbaseError(refreshErr);
|
|
485
|
+
const status = pe?.status ?? 0;
|
|
486
|
+
if (status === 400 || status === 401 || status === 403) {
|
|
487
|
+
rt.tokenManager.clearSession();
|
|
488
|
+
record(res.error.status);
|
|
489
|
+
throw fromPalbaseError(res.error);
|
|
490
|
+
}
|
|
491
|
+
record(pe?.status ?? 0);
|
|
492
|
+
throw pe ? fromPalbaseError(pe) : refreshErr;
|
|
446
493
|
}
|
|
447
|
-
|
|
494
|
+
res = await attempt();
|
|
448
495
|
}
|
|
449
|
-
|
|
496
|
+
} catch (e) {
|
|
497
|
+
if (!isAbort(e)) record(asPalbaseError(e)?.status ?? 0);
|
|
498
|
+
throw e;
|
|
450
499
|
}
|
|
500
|
+
record(res.error?.status ?? 200);
|
|
451
501
|
return unwrap(res);
|
|
452
502
|
}
|
|
453
503
|
|
|
@@ -464,7 +514,7 @@ function palbeState() {
|
|
|
464
514
|
}
|
|
465
515
|
|
|
466
516
|
// src/namespaces.ts
|
|
467
|
-
var
|
|
517
|
+
var RESERVED = /* @__PURE__ */ new Set([
|
|
468
518
|
"call",
|
|
469
519
|
"upload",
|
|
470
520
|
"auth",
|
|
@@ -472,7 +522,8 @@ var FIXED_SURFACE = /* @__PURE__ */ new Set([
|
|
|
472
522
|
"realtime",
|
|
473
523
|
"analytics",
|
|
474
524
|
"calls",
|
|
475
|
-
"messaging"
|
|
525
|
+
"messaging",
|
|
526
|
+
"perf"
|
|
476
527
|
]);
|
|
477
528
|
function reservedNamespaceError(key) {
|
|
478
529
|
return new BackendError("validation", {
|
|
@@ -537,7 +588,7 @@ function validateTree(node) {
|
|
|
537
588
|
}
|
|
538
589
|
function __registerNamespaces(tree) {
|
|
539
590
|
for (const key of Object.keys(tree)) {
|
|
540
|
-
if (
|
|
591
|
+
if (RESERVED.has(key)) throw reservedNamespaceError(key);
|
|
541
592
|
}
|
|
542
593
|
validateTree(tree);
|
|
543
594
|
const state = palbeState();
|
|
@@ -1386,6 +1437,23 @@ var PalbeAnalytics = class {
|
|
|
1386
1437
|
}
|
|
1387
1438
|
};
|
|
1388
1439
|
|
|
1440
|
+
// src/app-config.ts
|
|
1441
|
+
function loadAppConfig(raw) {
|
|
1442
|
+
if (typeof raw !== "object" || raw === null) {
|
|
1443
|
+
throw new Error("app_config_invalid: expected a JSON object");
|
|
1444
|
+
}
|
|
1445
|
+
const r = raw;
|
|
1446
|
+
const str = (k) => typeof r[k] === "string" ? r[k] : "";
|
|
1447
|
+
return { appId: str("app_id"), identifier: str("identifier"), envPreset: str("env_preset") };
|
|
1448
|
+
}
|
|
1449
|
+
function assertOriginMatches(cfg, runtimeOrigin) {
|
|
1450
|
+
if (cfg.identifier === "") return;
|
|
1451
|
+
if (runtimeOrigin === "") return;
|
|
1452
|
+
if (runtimeOrigin !== cfg.identifier) {
|
|
1453
|
+
throw new Error(`app_config_mismatch: expected origin ${cfg.identifier}, got ${runtimeOrigin}`);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1389
1457
|
// src/auth-wire.ts
|
|
1390
1458
|
function asWireAuthResult(raw) {
|
|
1391
1459
|
if (typeof raw !== "object" || raw === null) return null;
|
|
@@ -1444,6 +1512,8 @@ var PalbeAuth = class {
|
|
|
1444
1512
|
// suppresses AuthClient's TOKEN_REFRESHED during re-signIn
|
|
1445
1513
|
signedInState = false;
|
|
1446
1514
|
// dedupes AuthClient's repeated SIGNED_OUT events
|
|
1515
|
+
hydratingUser = false;
|
|
1516
|
+
// guards hydrateUser() to a single boot fetch
|
|
1447
1517
|
stateListeners = /* @__PURE__ */ new Set();
|
|
1448
1518
|
eventListeners = /* @__PURE__ */ new Set();
|
|
1449
1519
|
userListeners = /* @__PURE__ */ new Set();
|
|
@@ -1496,13 +1566,37 @@ var PalbeAuth = class {
|
|
|
1496
1566
|
if (changed) for (const cb of this.userListeners) this.safeInvoke(() => cb(user));
|
|
1497
1567
|
return user;
|
|
1498
1568
|
}
|
|
1569
|
+
/**
|
|
1570
|
+
* Restore the user after a session was rehydrated from storage (page reload).
|
|
1571
|
+
* Hydration in buildRuntime restores the TOKENS synchronously, but the access
|
|
1572
|
+
* token JWT does not carry emailVerified/createdAt, so the full AuthUser can't
|
|
1573
|
+
* be reconstructed offline — this fetches GET /auth/user once and announces
|
|
1574
|
+
* `signedIn`, so the app no longer has to `await pb.auth.refreshUser()` itself
|
|
1575
|
+
* on boot. Idempotent (runs once), browser-safe (no-op when not signed in or
|
|
1576
|
+
* a user is already cached), and never throws (a boot-time network failure
|
|
1577
|
+
* must not break app startup — isSignedIn stays true, the app can retry).
|
|
1578
|
+
*/
|
|
1579
|
+
hydrateUser() {
|
|
1580
|
+
if (this.hydratingUser || this.cachedUser || !this.isSignedIn) return;
|
|
1581
|
+
this.hydratingUser = true;
|
|
1582
|
+
void this.refreshUser().then((user) => {
|
|
1583
|
+
if (this.isSignedIn) {
|
|
1584
|
+
this.signedInState = true;
|
|
1585
|
+
this.emitState({ status: "signedIn", user });
|
|
1586
|
+
}
|
|
1587
|
+
}).catch(() => {
|
|
1588
|
+
}).finally(() => {
|
|
1589
|
+
this.hydratingUser = false;
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1499
1592
|
// ── listeners ──────────────────────────────────────────
|
|
1500
1593
|
/**
|
|
1501
1594
|
* Subscribe to signed-in/signed-out state. Fires immediately with the
|
|
1502
|
-
* current snapshot (iOS parity).
|
|
1503
|
-
*
|
|
1504
|
-
*
|
|
1505
|
-
*
|
|
1595
|
+
* current snapshot (iOS parity). A restored session (page reload) hydrates
|
|
1596
|
+
* the user asynchronously via `hydrateUser()` (fired once at boot), so the
|
|
1597
|
+
* FIRST snapshot may report signedOut for a tick even when `isSignedIn` is
|
|
1598
|
+
* true; the listener then fires again with `signedIn` once the user lands.
|
|
1599
|
+
* Rely on `isSignedIn` for the session truth if you need it synchronously.
|
|
1506
1600
|
*/
|
|
1507
1601
|
onAuthStateChange(callback) {
|
|
1508
1602
|
this.stateListeners.add(callback);
|
|
@@ -7706,6 +7800,469 @@ var PalbeMessaging = class {
|
|
|
7706
7800
|
}
|
|
7707
7801
|
};
|
|
7708
7802
|
|
|
7803
|
+
// src/perf/app-start.ts
|
|
7804
|
+
function measureAppStart(nav) {
|
|
7805
|
+
if (!(nav.fcp > 0)) return null;
|
|
7806
|
+
const value = nav.fcp - nav.startTime;
|
|
7807
|
+
if (value <= 0) return null;
|
|
7808
|
+
return {
|
|
7809
|
+
row_id: crypto.randomUUID(),
|
|
7810
|
+
trace_type: "app_start",
|
|
7811
|
+
name: "cold_start",
|
|
7812
|
+
value,
|
|
7813
|
+
timestamp: Date.now()
|
|
7814
|
+
};
|
|
7815
|
+
}
|
|
7816
|
+
|
|
7817
|
+
// src/perf/perf-config-client.ts
|
|
7818
|
+
var PERF_CONFIG_PATH = "/v1/analytics/perf/config";
|
|
7819
|
+
var FNV_OFFSET_BASIS_32 = 2166136261;
|
|
7820
|
+
var FNV_PRIME_32 = 16777619;
|
|
7821
|
+
var utf82 = typeof TextEncoder !== "undefined" ? new TextEncoder() : { encode: (s) => Uint8Array.from(Buffer.from(s, "utf-8")) };
|
|
7822
|
+
function perfSampleBucket(rowId) {
|
|
7823
|
+
let hash = FNV_OFFSET_BASIS_32;
|
|
7824
|
+
for (const byte of utf82.encode(rowId)) {
|
|
7825
|
+
hash ^= byte;
|
|
7826
|
+
hash = Math.imul(hash, FNV_PRIME_32);
|
|
7827
|
+
}
|
|
7828
|
+
return (hash >>> 0) % 100;
|
|
7829
|
+
}
|
|
7830
|
+
function isPerfConfig(value) {
|
|
7831
|
+
if (typeof value !== "object" || value === null) return false;
|
|
7832
|
+
const o = value;
|
|
7833
|
+
return typeof o.sample_pct === "number";
|
|
7834
|
+
}
|
|
7835
|
+
function isRawResponse(value) {
|
|
7836
|
+
return typeof value === "object" && value !== null && "data" in value;
|
|
7837
|
+
}
|
|
7838
|
+
function headerValue(headers, name) {
|
|
7839
|
+
if (!headers) return void 0;
|
|
7840
|
+
const lower = name.toLowerCase();
|
|
7841
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
7842
|
+
if (k.toLowerCase() === lower) return v;
|
|
7843
|
+
}
|
|
7844
|
+
return void 0;
|
|
7845
|
+
}
|
|
7846
|
+
var PerfConfigClient = class {
|
|
7847
|
+
etag;
|
|
7848
|
+
async fetchConfig(rt, perf) {
|
|
7849
|
+
const headers = {};
|
|
7850
|
+
if (this.etag) headers["If-None-Match"] = this.etag;
|
|
7851
|
+
try {
|
|
7852
|
+
const res = await rt.http.request("GET", PERF_CONFIG_PATH, { headers });
|
|
7853
|
+
if (!isRawResponse(res)) return;
|
|
7854
|
+
if (res.status === 304 || res.error && res.error.status === 304) return;
|
|
7855
|
+
if (res.error) return;
|
|
7856
|
+
const newEtag = headerValue(res.headers, "etag");
|
|
7857
|
+
if (newEtag) this.etag = newEtag;
|
|
7858
|
+
if (isPerfConfig(res.data)) {
|
|
7859
|
+
perf.setSamplePct(res.data.sample_pct);
|
|
7860
|
+
}
|
|
7861
|
+
} catch {
|
|
7862
|
+
}
|
|
7863
|
+
}
|
|
7864
|
+
};
|
|
7865
|
+
|
|
7866
|
+
// src/perf/fetch-swizzle.ts
|
|
7867
|
+
var PERF_EXCLUDED_PREFIX2 = "/v1/analytics/";
|
|
7868
|
+
function nowMs2() {
|
|
7869
|
+
return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
7870
|
+
}
|
|
7871
|
+
function describeRequest(input, init) {
|
|
7872
|
+
let url;
|
|
7873
|
+
let method = "GET";
|
|
7874
|
+
if (typeof input === "string") {
|
|
7875
|
+
url = input;
|
|
7876
|
+
} else if (input instanceof URL) {
|
|
7877
|
+
url = input.href;
|
|
7878
|
+
} else {
|
|
7879
|
+
url = input.url;
|
|
7880
|
+
method = input.method;
|
|
7881
|
+
}
|
|
7882
|
+
if (init?.method) method = init.method;
|
|
7883
|
+
return { url, method: method.toUpperCase() };
|
|
7884
|
+
}
|
|
7885
|
+
function pathOf(url) {
|
|
7886
|
+
try {
|
|
7887
|
+
return new URL(url, "http://_local").pathname;
|
|
7888
|
+
} catch {
|
|
7889
|
+
return url;
|
|
7890
|
+
}
|
|
7891
|
+
}
|
|
7892
|
+
function installFetchSwizzle(perf) {
|
|
7893
|
+
if (typeof fetch !== "function") return () => {
|
|
7894
|
+
};
|
|
7895
|
+
const original = fetch;
|
|
7896
|
+
const wrapped = async (input, init) => {
|
|
7897
|
+
const { url, method } = describeRequest(input, init);
|
|
7898
|
+
const traced = !pathOf(url).startsWith(PERF_EXCLUDED_PREFIX2);
|
|
7899
|
+
const startedAt = traced ? nowMs2() : 0;
|
|
7900
|
+
try {
|
|
7901
|
+
const res = await original(input, init);
|
|
7902
|
+
if (traced) perf.recordNetwork(method, redactUrl(url), res.status, nowMs2() - startedAt);
|
|
7903
|
+
return res;
|
|
7904
|
+
} catch (err) {
|
|
7905
|
+
if (traced) perf.recordNetwork(method, redactUrl(url), 0, nowMs2() - startedAt);
|
|
7906
|
+
throw err;
|
|
7907
|
+
}
|
|
7908
|
+
};
|
|
7909
|
+
globalThis.fetch = wrapped;
|
|
7910
|
+
return () => {
|
|
7911
|
+
globalThis.fetch = original;
|
|
7912
|
+
};
|
|
7913
|
+
}
|
|
7914
|
+
|
|
7915
|
+
// src/perf/offline-queue.ts
|
|
7916
|
+
var PERF_QUEUE_KEY = "palbe.perf.queue";
|
|
7917
|
+
var DEFAULT_MAX_ITEMS = 500;
|
|
7918
|
+
function canPersist() {
|
|
7919
|
+
return typeof document !== "undefined" && typeof localStorage !== "undefined";
|
|
7920
|
+
}
|
|
7921
|
+
function isPerfItem(value) {
|
|
7922
|
+
if (typeof value !== "object" || value === null) return false;
|
|
7923
|
+
const o = value;
|
|
7924
|
+
return typeof o.row_id === "string" && typeof o.trace_type === "string" && typeof o.name === "string" && typeof o.value === "number" && typeof o.timestamp === "number";
|
|
7925
|
+
}
|
|
7926
|
+
function readPersisted() {
|
|
7927
|
+
if (!canPersist()) return [];
|
|
7928
|
+
try {
|
|
7929
|
+
const raw = localStorage.getItem(PERF_QUEUE_KEY);
|
|
7930
|
+
if (!raw) return [];
|
|
7931
|
+
const parsed = JSON.parse(raw);
|
|
7932
|
+
if (!Array.isArray(parsed)) return [];
|
|
7933
|
+
return parsed.filter(isPerfItem);
|
|
7934
|
+
} catch {
|
|
7935
|
+
return [];
|
|
7936
|
+
}
|
|
7937
|
+
}
|
|
7938
|
+
var PerfOfflineQueue = class {
|
|
7939
|
+
items;
|
|
7940
|
+
_dropped = 0;
|
|
7941
|
+
maxItems;
|
|
7942
|
+
constructor(maxItems = DEFAULT_MAX_ITEMS) {
|
|
7943
|
+
this.maxItems = Math.max(1, maxItems);
|
|
7944
|
+
this.items = readPersisted();
|
|
7945
|
+
this.trim();
|
|
7946
|
+
}
|
|
7947
|
+
/** Append items; FIFO-evict the oldest when over `maxItems`. */
|
|
7948
|
+
enqueue(items) {
|
|
7949
|
+
if (items.length === 0) return;
|
|
7950
|
+
this.items.push(...items);
|
|
7951
|
+
this.trim();
|
|
7952
|
+
this.persist();
|
|
7953
|
+
}
|
|
7954
|
+
/** Return all queued items (oldest-first) and clear the queue + store. */
|
|
7955
|
+
drainAll() {
|
|
7956
|
+
if (this.items.length === 0) return [];
|
|
7957
|
+
const out = this.items;
|
|
7958
|
+
this.items = [];
|
|
7959
|
+
this.clearStore();
|
|
7960
|
+
return out;
|
|
7961
|
+
}
|
|
7962
|
+
/** Current queue depth. */
|
|
7963
|
+
get count() {
|
|
7964
|
+
return this.items.length;
|
|
7965
|
+
}
|
|
7966
|
+
/** Cumulative count of FIFO-evicted items (a `dropped` metric, not silent). */
|
|
7967
|
+
get dropped() {
|
|
7968
|
+
return this._dropped;
|
|
7969
|
+
}
|
|
7970
|
+
/** Drop the oldest items until at most `maxItems` remain, counting each. */
|
|
7971
|
+
trim() {
|
|
7972
|
+
const overflow = this.items.length - this.maxItems;
|
|
7973
|
+
if (overflow > 0) {
|
|
7974
|
+
this.items.splice(0, overflow);
|
|
7975
|
+
this._dropped += overflow;
|
|
7976
|
+
}
|
|
7977
|
+
}
|
|
7978
|
+
persist() {
|
|
7979
|
+
if (!canPersist()) return;
|
|
7980
|
+
try {
|
|
7981
|
+
localStorage.setItem(PERF_QUEUE_KEY, JSON.stringify(this.items));
|
|
7982
|
+
} catch {
|
|
7983
|
+
}
|
|
7984
|
+
}
|
|
7985
|
+
clearStore() {
|
|
7986
|
+
if (!canPersist()) return;
|
|
7987
|
+
try {
|
|
7988
|
+
localStorage.removeItem(PERF_QUEUE_KEY);
|
|
7989
|
+
} catch {
|
|
7990
|
+
}
|
|
7991
|
+
}
|
|
7992
|
+
};
|
|
7993
|
+
|
|
7994
|
+
// src/perf/perf-state.ts
|
|
7995
|
+
var MAX_PERF_BATCH = 100;
|
|
7996
|
+
var PerfState = class {
|
|
7997
|
+
/** Pending, un-flushed perf items (FIFO). */
|
|
7998
|
+
buffer = [];
|
|
7999
|
+
/** When true, every flush carries `X-Palbase-Test-Device: 1`. */
|
|
8000
|
+
testDevice = false;
|
|
8001
|
+
enqueue(item) {
|
|
8002
|
+
this.buffer.push(item);
|
|
8003
|
+
}
|
|
8004
|
+
/** Detach up to `MAX_PERF_BATCH` items for one POST (FIFO order preserved). */
|
|
8005
|
+
take(limit = MAX_PERF_BATCH) {
|
|
8006
|
+
return this.buffer.splice(0, limit);
|
|
8007
|
+
}
|
|
8008
|
+
get size() {
|
|
8009
|
+
return this.buffer.length;
|
|
8010
|
+
}
|
|
8011
|
+
};
|
|
8012
|
+
|
|
8013
|
+
// src/perf/perf-wire.ts
|
|
8014
|
+
function encodePerfBatch(items) {
|
|
8015
|
+
return { items };
|
|
8016
|
+
}
|
|
8017
|
+
|
|
8018
|
+
// src/perf/perf-facade.ts
|
|
8019
|
+
var FLUSH_AT2 = 20;
|
|
8020
|
+
var FLUSH_INTERVAL_MS2 = 1e4;
|
|
8021
|
+
var TEST_DEVICE_HEADER = "X-Palbase-Test-Device";
|
|
8022
|
+
function nowMs3() {
|
|
8023
|
+
return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
8024
|
+
}
|
|
8025
|
+
var PerfTrace = class {
|
|
8026
|
+
constructor(name, onStop) {
|
|
8027
|
+
this.name = name;
|
|
8028
|
+
this.onStop = onStop;
|
|
8029
|
+
}
|
|
8030
|
+
name;
|
|
8031
|
+
onStop;
|
|
8032
|
+
attrs = {};
|
|
8033
|
+
counters = {};
|
|
8034
|
+
startedAt = nowMs3();
|
|
8035
|
+
stopped = false;
|
|
8036
|
+
putAttribute(key, value) {
|
|
8037
|
+
this.attrs[key] = value;
|
|
8038
|
+
}
|
|
8039
|
+
incrementMetric(name, by = 1) {
|
|
8040
|
+
this.counters[name] = (this.counters[name] ?? 0) + by;
|
|
8041
|
+
}
|
|
8042
|
+
stop() {
|
|
8043
|
+
if (this.stopped) return;
|
|
8044
|
+
this.stopped = true;
|
|
8045
|
+
const item = {
|
|
8046
|
+
row_id: crypto.randomUUID(),
|
|
8047
|
+
trace_type: "custom",
|
|
8048
|
+
name: this.name,
|
|
8049
|
+
value: Math.max(0, nowMs3() - this.startedAt),
|
|
8050
|
+
timestamp: Date.now()
|
|
8051
|
+
};
|
|
8052
|
+
if (Object.keys(this.attrs).length > 0) item.attrs = this.attrs;
|
|
8053
|
+
if (Object.keys(this.counters).length > 0) item.counters = this.counters;
|
|
8054
|
+
this.onStop(item);
|
|
8055
|
+
}
|
|
8056
|
+
};
|
|
8057
|
+
var PalbePerf = class {
|
|
8058
|
+
constructor(rt, state = new PerfState(), queue = new PerfOfflineQueue()) {
|
|
8059
|
+
this.rt = rt;
|
|
8060
|
+
this.state = state;
|
|
8061
|
+
this.queue = queue;
|
|
8062
|
+
if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
|
|
8063
|
+
window.addEventListener("online", this.onOnline);
|
|
8064
|
+
}
|
|
8065
|
+
}
|
|
8066
|
+
rt;
|
|
8067
|
+
state;
|
|
8068
|
+
queue;
|
|
8069
|
+
flushTimer = null;
|
|
8070
|
+
/** Browser → buffer + size/timer flush. Server (no document) → immediate
|
|
8071
|
+
* per-item flush, zero timers (nothing leaks into RSC/route handlers). */
|
|
8072
|
+
browser = typeof document !== "undefined";
|
|
8073
|
+
/** Bound `online` handler so it can be removed on `dispose()` (no leak). */
|
|
8074
|
+
onOnline = () => {
|
|
8075
|
+
void this.flush();
|
|
8076
|
+
};
|
|
8077
|
+
/** Server-controlled client-side sample rate (0..100). 100 until the config
|
|
8078
|
+
* client fetches `/v1/analytics/perf/config` — the SDK obeys this ceiling and
|
|
8079
|
+
* never raises its own rate (invariant 3). `record` drops an item whose
|
|
8080
|
+
* deterministic `row_id` bucket is >= this pct, mirroring the server's
|
|
8081
|
+
* `SampleDecision` so client + server keep the SAME rows. */
|
|
8082
|
+
samplePct = 100;
|
|
8083
|
+
/** Remove the `online` listener. Called when the runtime is replaced so the
|
|
8084
|
+
* handler does not outlive this facade. No-op outside the browser. */
|
|
8085
|
+
dispose() {
|
|
8086
|
+
if (typeof window !== "undefined" && typeof window.removeEventListener === "function") {
|
|
8087
|
+
window.removeEventListener("online", this.onOnline);
|
|
8088
|
+
}
|
|
8089
|
+
}
|
|
8090
|
+
/** Mark (or unmark) this client's traffic as test — the server tags the rows
|
|
8091
|
+
* when the `X-Palbase-Test-Device: 1` header rides along on flush. */
|
|
8092
|
+
setTestDevice(on) {
|
|
8093
|
+
this.state.testDevice = on;
|
|
8094
|
+
}
|
|
8095
|
+
/** Apply the server-resolved client-side sample rate (0..100), clamped. Called
|
|
8096
|
+
* by `PerfConfigClient` after fetching `/v1/analytics/perf/config`. The SDK
|
|
8097
|
+
* OBEYS this value — it is a ceiling, never raised locally. */
|
|
8098
|
+
setSamplePct(pct) {
|
|
8099
|
+
this.samplePct = Math.max(0, Math.min(100, Math.trunc(pct)));
|
|
8100
|
+
}
|
|
8101
|
+
/** Pending (un-flushed) buffer depth. Test-only visibility into the buffer so
|
|
8102
|
+
* the remote-sampling tests can assert how many items were sampled in. */
|
|
8103
|
+
get bufferSizeForTest() {
|
|
8104
|
+
return this.state.size;
|
|
8105
|
+
}
|
|
8106
|
+
/** Opt-in: wrap the global `fetch` so every app-level request is recorded as a
|
|
8107
|
+
* redacted `network` perf item (the `/v1/analytics/*` ingest paths are
|
|
8108
|
+
* self-excluded). OFF by default — swizzling a global is a page-wide side
|
|
8109
|
+
* effect. Returns an `uninstall` that restores the original `fetch`. */
|
|
8110
|
+
enableFetchCapture() {
|
|
8111
|
+
return installFetchSwizzle(this);
|
|
8112
|
+
}
|
|
8113
|
+
/** Start a custom trace; the returned handle records a `custom` item on
|
|
8114
|
+
* `.stop()`. */
|
|
8115
|
+
startTrace(name) {
|
|
8116
|
+
return new PerfTrace(name, (item) => this.record(item));
|
|
8117
|
+
}
|
|
8118
|
+
/** Buffer one perf item. In the browser, flush on size/timer; on the server
|
|
8119
|
+
* flush immediately (no timers). Never throws.
|
|
8120
|
+
*
|
|
8121
|
+
* Client-side remote sampling: an item whose deterministic `row_id` bucket is
|
|
8122
|
+
* NOT below the server-controlled `samplePct` is dropped before buffering —
|
|
8123
|
+
* the SAME FNV-1a-mod-100 decision the server applies at ingest, so the two
|
|
8124
|
+
* keep identical rows and the SDK saves the upload (defense-in-depth: the
|
|
8125
|
+
* server re-samples authoritatively). */
|
|
8126
|
+
record(item) {
|
|
8127
|
+
if (perfSampleBucket(item.row_id) >= this.samplePct) return;
|
|
8128
|
+
this.state.enqueue(item);
|
|
8129
|
+
if (!this.browser) {
|
|
8130
|
+
void this.flush();
|
|
8131
|
+
return;
|
|
8132
|
+
}
|
|
8133
|
+
if (this.state.size >= FLUSH_AT2) void this.flush();
|
|
8134
|
+
else this.startTimer();
|
|
8135
|
+
}
|
|
8136
|
+
/** Buffer a network trace — called by `request.ts` around `rt.http.request`
|
|
8137
|
+
* (the analytics ingest path is excluded by the caller to avoid recursion). */
|
|
8138
|
+
recordNetwork(method, url, status, durationMs, requestId) {
|
|
8139
|
+
const item = {
|
|
8140
|
+
row_id: crypto.randomUUID(),
|
|
8141
|
+
trace_type: "network",
|
|
8142
|
+
name: `${method} ${url}`,
|
|
8143
|
+
value: durationMs,
|
|
8144
|
+
attrs: { status: String(status) },
|
|
8145
|
+
timestamp: Date.now()
|
|
8146
|
+
};
|
|
8147
|
+
if (requestId) item.request_id = requestId;
|
|
8148
|
+
this.record(item);
|
|
8149
|
+
}
|
|
8150
|
+
/** Drain the offline queue (oldest-first) and the live buffer to
|
|
8151
|
+
* `/v1/analytics/perf` (≤100 items per request, sequential). Resolves when
|
|
8152
|
+
* delivery finished; NEVER rejects. A failed slice is NOT dropped — it goes
|
|
8153
|
+
* to the offline queue (persist-on-fail) so the next flush (size/timer or the
|
|
8154
|
+
* `online` reconnect event) retries it. Items keep their original `row_id`,
|
|
8155
|
+
* so a redelivery dedups server-side (ReplacingMergeTree). */
|
|
8156
|
+
async flush() {
|
|
8157
|
+
this.cancelTimer();
|
|
8158
|
+
const pending = this.queue.drainAll();
|
|
8159
|
+
if (pending.length === 0 && this.state.size === 0) return;
|
|
8160
|
+
const headers = this.state.testDevice ? { [TEST_DEVICE_HEADER]: "1" } : void 0;
|
|
8161
|
+
const remaining = [...pending];
|
|
8162
|
+
while (this.state.size > 0) remaining.push(...this.state.take(MAX_PERF_BATCH));
|
|
8163
|
+
for (let i = 0; i < remaining.length; i += MAX_PERF_BATCH) {
|
|
8164
|
+
const slice = remaining.slice(i, i + MAX_PERF_BATCH);
|
|
8165
|
+
try {
|
|
8166
|
+
await palbeRequest(
|
|
8167
|
+
this.rt,
|
|
8168
|
+
"POST",
|
|
8169
|
+
"/v1/analytics/perf",
|
|
8170
|
+
{ body: encodePerfBatch(slice), headers }
|
|
8171
|
+
);
|
|
8172
|
+
} catch {
|
|
8173
|
+
this.queue.enqueue(slice);
|
|
8174
|
+
}
|
|
8175
|
+
}
|
|
8176
|
+
}
|
|
8177
|
+
startTimer() {
|
|
8178
|
+
if (this.flushTimer !== null) return;
|
|
8179
|
+
this.flushTimer = setTimeout(() => {
|
|
8180
|
+
this.flushTimer = null;
|
|
8181
|
+
void this.flush();
|
|
8182
|
+
}, FLUSH_INTERVAL_MS2);
|
|
8183
|
+
}
|
|
8184
|
+
cancelTimer() {
|
|
8185
|
+
if (this.flushTimer !== null) {
|
|
8186
|
+
clearTimeout(this.flushTimer);
|
|
8187
|
+
this.flushTimer = null;
|
|
8188
|
+
}
|
|
8189
|
+
}
|
|
8190
|
+
};
|
|
8191
|
+
|
|
8192
|
+
// src/perf/web-vitals.ts
|
|
8193
|
+
function webVitalItem(name, value) {
|
|
8194
|
+
return {
|
|
8195
|
+
row_id: crypto.randomUUID(),
|
|
8196
|
+
trace_type: "web_vital",
|
|
8197
|
+
name,
|
|
8198
|
+
value: Math.max(0, value),
|
|
8199
|
+
timestamp: Date.now()
|
|
8200
|
+
};
|
|
8201
|
+
}
|
|
8202
|
+
function isLayoutShift(e) {
|
|
8203
|
+
return e.entryType === "layout-shift" && "value" in e && "hadRecentInput" in e;
|
|
8204
|
+
}
|
|
8205
|
+
function isEventTiming(e) {
|
|
8206
|
+
return e.entryType === "event" || e.entryType === "first-input";
|
|
8207
|
+
}
|
|
8208
|
+
function safeObserve(type, cb) {
|
|
8209
|
+
if (typeof PerformanceObserver === "undefined") return null;
|
|
8210
|
+
try {
|
|
8211
|
+
const obs = new PerformanceObserver((list) => cb(list.getEntries()));
|
|
8212
|
+
obs.observe({ type, buffered: true });
|
|
8213
|
+
return obs;
|
|
8214
|
+
} catch {
|
|
8215
|
+
return null;
|
|
8216
|
+
}
|
|
8217
|
+
}
|
|
8218
|
+
function observeWebVitals(record) {
|
|
8219
|
+
if (typeof document === "undefined" || typeof document.addEventListener !== "function" || typeof document.removeEventListener !== "function" || typeof performance === "undefined" || typeof performance.getEntriesByType !== "function") {
|
|
8220
|
+
return () => {
|
|
8221
|
+
};
|
|
8222
|
+
}
|
|
8223
|
+
let cls = 0;
|
|
8224
|
+
let lcp = 0;
|
|
8225
|
+
let inp = 0;
|
|
8226
|
+
const observers = [
|
|
8227
|
+
// LCP: keep the largest/last reported render.
|
|
8228
|
+
safeObserve("largest-contentful-paint", (entries) => {
|
|
8229
|
+
for (const e of entries) lcp = Math.max(lcp, e.startTime);
|
|
8230
|
+
}),
|
|
8231
|
+
// CLS: sum shift values that weren't caused by recent input.
|
|
8232
|
+
safeObserve("layout-shift", (entries) => {
|
|
8233
|
+
for (const e of entries) if (isLayoutShift(e) && !e.hadRecentInput) cls += e.value;
|
|
8234
|
+
}),
|
|
8235
|
+
// INP: approximate as the worst interaction duration observed.
|
|
8236
|
+
safeObserve("event", (entries) => {
|
|
8237
|
+
for (const e of entries) if (isEventTiming(e)) inp = Math.max(inp, e.duration);
|
|
8238
|
+
}),
|
|
8239
|
+
// FCP: one-shot.
|
|
8240
|
+
safeObserve("paint", (entries) => {
|
|
8241
|
+
for (const e of entries)
|
|
8242
|
+
if (e.name === "first-contentful-paint") record(webVitalItem("FCP", e.startTime));
|
|
8243
|
+
})
|
|
8244
|
+
];
|
|
8245
|
+
const nav = performance.getEntriesByType("navigation")[0];
|
|
8246
|
+
if (nav && nav.responseStart > 0) record(webVitalItem("TTFB", nav.responseStart));
|
|
8247
|
+
let flushed = false;
|
|
8248
|
+
const flush = () => {
|
|
8249
|
+
if (flushed) return;
|
|
8250
|
+
flushed = true;
|
|
8251
|
+
if (lcp > 0) record(webVitalItem("LCP", lcp));
|
|
8252
|
+
record(webVitalItem("CLS", cls));
|
|
8253
|
+
if (inp > 0) record(webVitalItem("INP", inp));
|
|
8254
|
+
};
|
|
8255
|
+
const onHide = () => {
|
|
8256
|
+
if (document.visibilityState === "hidden") flush();
|
|
8257
|
+
};
|
|
8258
|
+
document.addEventListener("visibilitychange", onHide);
|
|
8259
|
+
return () => {
|
|
8260
|
+
flush();
|
|
8261
|
+
document.removeEventListener("visibilitychange", onHide);
|
|
8262
|
+
for (const o of observers) o?.disconnect();
|
|
8263
|
+
};
|
|
8264
|
+
}
|
|
8265
|
+
|
|
7709
8266
|
// src/realtime/anon-token.ts
|
|
7710
8267
|
var REFRESH_SKEW_MS = 6e4;
|
|
7711
8268
|
var AnonTokenProvider = class {
|
|
@@ -8393,10 +8950,13 @@ function defaultSessionStorage(key) {
|
|
|
8393
8950
|
}
|
|
8394
8951
|
|
|
8395
8952
|
// src/version.ts
|
|
8396
|
-
var VERSION = "1.
|
|
8953
|
+
var VERSION = "1.9.0";
|
|
8397
8954
|
|
|
8398
8955
|
// src/runtime.ts
|
|
8399
8956
|
function buildRuntime(config) {
|
|
8957
|
+
const appIdentifier = config.identifier ?? "";
|
|
8958
|
+
const runtimeOrigin = typeof window !== "undefined" && typeof window.location !== "undefined" ? window.location.origin : "";
|
|
8959
|
+
assertOriginMatches(loadAppConfig({ identifier: appIdentifier }), runtimeOrigin);
|
|
8400
8960
|
const http = new HttpClient(config.apiKey, {
|
|
8401
8961
|
url: config.url,
|
|
8402
8962
|
headers: { "X-Client-Info": `palbe-web/${VERSION}`, ...config.headers }
|
|
@@ -8446,8 +9006,10 @@ function buildRuntime(config) {
|
|
|
8446
9006
|
let analytics;
|
|
8447
9007
|
let calls;
|
|
8448
9008
|
let messaging;
|
|
9009
|
+
let perf;
|
|
8449
9010
|
const rt = {
|
|
8450
9011
|
config,
|
|
9012
|
+
appIdentifier,
|
|
8451
9013
|
http,
|
|
8452
9014
|
tokenManager,
|
|
8453
9015
|
authClient,
|
|
@@ -8483,11 +9045,19 @@ function buildRuntime(config) {
|
|
|
8483
9045
|
destroyRealtime() {
|
|
8484
9046
|
realtime?.destroy();
|
|
8485
9047
|
realtime = void 0;
|
|
9048
|
+
perf?.dispose();
|
|
8486
9049
|
},
|
|
8487
9050
|
// The buffering facade is lazy; its identity state is NOT (below).
|
|
8488
9051
|
get analytics() {
|
|
8489
9052
|
if (!analytics) analytics = new PalbeAnalytics(rt, analyticsState);
|
|
8490
9053
|
return analytics;
|
|
9054
|
+
},
|
|
9055
|
+
// PalPerf is constructed up front (touched below): `request.ts` records a
|
|
9056
|
+
// network trace on EVERY fetch, so it can't be lazy. The memo here only
|
|
9057
|
+
// guards against re-construction.
|
|
9058
|
+
get perf() {
|
|
9059
|
+
if (!perf) perf = new PalbePerf(rt);
|
|
9060
|
+
return perf;
|
|
8491
9061
|
}
|
|
8492
9062
|
};
|
|
8493
9063
|
const analyticsState = new AnalyticsState(rt);
|
|
@@ -8495,8 +9065,43 @@ function buildRuntime(config) {
|
|
|
8495
9065
|
const hasOwn = Object.keys(request.headers).some((k) => k.toLowerCase() === "x-distinct-id");
|
|
8496
9066
|
if (!hasOwn) request.headers["X-Distinct-Id"] = analyticsState.distinctId();
|
|
8497
9067
|
});
|
|
9068
|
+
void rt.perf;
|
|
9069
|
+
if (typeof document !== "undefined") {
|
|
9070
|
+
void new PerfConfigClient().fetchConfig(rt, rt.perf);
|
|
9071
|
+
rt.auth.hydrateUser();
|
|
9072
|
+
}
|
|
9073
|
+
recordColdStart(rt);
|
|
9074
|
+
observeWebVitals((item) => rt.perf.record(item));
|
|
8498
9075
|
return rt;
|
|
8499
9076
|
}
|
|
9077
|
+
function recordColdStart(rt) {
|
|
9078
|
+
if (typeof document === "undefined" || typeof performance === "undefined") return;
|
|
9079
|
+
try {
|
|
9080
|
+
const navEntry = performance.getEntriesByType("navigation")[0];
|
|
9081
|
+
const startTime = navEntry?.startTime ?? 0;
|
|
9082
|
+
const recordFromFcp = (fcp) => {
|
|
9083
|
+
const item = measureAppStart({ startTime, fcp });
|
|
9084
|
+
if (item) rt.perf.record(item);
|
|
9085
|
+
};
|
|
9086
|
+
const existing = performance.getEntriesByName("first-contentful-paint").find((e) => e.startTime > 0);
|
|
9087
|
+
if (existing) {
|
|
9088
|
+
recordFromFcp(existing.startTime);
|
|
9089
|
+
return;
|
|
9090
|
+
}
|
|
9091
|
+
if (typeof PerformanceObserver === "undefined") return;
|
|
9092
|
+
const observer = new PerformanceObserver((list) => {
|
|
9093
|
+
for (const entry of list.getEntries()) {
|
|
9094
|
+
if (entry.name === "first-contentful-paint") {
|
|
9095
|
+
observer.disconnect();
|
|
9096
|
+
recordFromFcp(entry.startTime);
|
|
9097
|
+
return;
|
|
9098
|
+
}
|
|
9099
|
+
}
|
|
9100
|
+
});
|
|
9101
|
+
observer.observe({ type: "paint", buffered: true });
|
|
9102
|
+
} catch {
|
|
9103
|
+
}
|
|
9104
|
+
}
|
|
8500
9105
|
|
|
8501
9106
|
// src/call.ts
|
|
8502
9107
|
async function callEndpoint(resolveRt, name, input, options) {
|
|
@@ -8675,6 +9280,12 @@ function createClientProxy(resolveRt, nsAccessor) {
|
|
|
8675
9280
|
},
|
|
8676
9281
|
get messaging() {
|
|
8677
9282
|
return resolveRt().messaging;
|
|
9283
|
+
},
|
|
9284
|
+
get perf() {
|
|
9285
|
+
return resolveRt().perf;
|
|
9286
|
+
},
|
|
9287
|
+
setTestDevice(on) {
|
|
9288
|
+
resolveRt().perf.setTestDevice(on);
|
|
8678
9289
|
}
|
|
8679
9290
|
};
|
|
8680
9291
|
return new Proxy(base, {
|