@levo-so/core 0.1.58 → 0.1.60
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/index.d.ts +1763 -8
- package/dist/index.js +435 -89
- package/package.json +12 -12
- package/dist/client.d.ts +0 -82
- package/dist/constants/collection/defaultByKind.d.ts +0 -3
- package/dist/constants/collection/fieldInterfaceFormat.d.ts +0 -3
- package/dist/constants/collection/fieldInterfaces.d.ts +0 -6
- package/dist/constants/collection/fieldKind.d.ts +0 -26
- package/dist/constants/index.d.ts +0 -8
- package/dist/constants/integration.d.ts +0 -23
- package/dist/constants/media.d.ts +0 -3
- package/dist/constants/oauthProvider.d.ts +0 -3
- package/dist/control/audience.d.ts +0 -22
- package/dist/control/index.d.ts +0 -23
- package/dist/httpClient.d.ts +0 -13
- package/dist/logger/batch.d.ts +0 -9
- package/dist/logger/index.d.ts +0 -22
- package/dist/modules/blog.d.ts +0 -10
- package/dist/modules/collection.d.ts +0 -26
- package/dist/modules/media.d.ts +0 -11
- package/dist/modules/membership.d.ts +0 -21
- package/dist/types/audience.d.ts +0 -100
- package/dist/types/blog/author.d.ts +0 -30
- package/dist/types/blog/blogClient.d.ts +0 -51
- package/dist/types/blog/category.d.ts +0 -20
- package/dist/types/blog/index.d.ts +0 -64
- package/dist/types/blog/post.d.ts +0 -63
- package/dist/types/blog/tag.d.ts +0 -19
- package/dist/types/collection/collectionView.d.ts +0 -36
- package/dist/types/collection/draft.d.ts +0 -9
- package/dist/types/collection/index.d.ts +0 -7
- package/dist/types/collection/lemaCollection.d.ts +0 -95
- package/dist/types/collection/lemaField.d.ts +0 -55
- package/dist/types/collection/lemaSection.d.ts +0 -10
- package/dist/types/collection/relation.d.ts +0 -11
- package/dist/types/emoji.d.ts +0 -22
- package/dist/types/event.d.ts +0 -225
- package/dist/types/index.d.ts +0 -14
- package/dist/types/levo-query.d.ts +0 -104
- package/dist/types/location.d.ts +0 -6
- package/dist/types/media.d.ts +0 -56
- package/dist/types/membership/index.d.ts +0 -182
- package/dist/types/query.d.ts +0 -46
- package/dist/types/schema/field.d.ts +0 -56
- package/dist/types/schema/index.d.ts +0 -3
- package/dist/types/schema/schema.d.ts +0 -58
- package/dist/types/utils.d.ts +0 -8
- package/dist/types/workspace.d.ts +0 -172
- package/dist/utils/LevoError.d.ts +0 -21
- package/dist/utils/formatImagePath.d.ts +0 -3
- package/dist/utils/getLevoError.d.ts +0 -3
- package/dist/utils/getUserProperties.d.ts +0 -8
- package/dist/utils/isIncognito.d.ts +0 -9
- package/dist/utils/listToX.d.ts +0 -3
- package/dist/utils/lock.d.ts +0 -21
package/dist/index.js
CHANGED
|
@@ -356,58 +356,282 @@ var getLevoError = (error) => {
|
|
|
356
356
|
});
|
|
357
357
|
};
|
|
358
358
|
|
|
359
|
-
// src/utils/lock.ts
|
|
360
|
-
var LocalStorageLock = class {
|
|
361
|
-
key;
|
|
362
|
-
constructor(lockKey) {
|
|
363
|
-
this.key = lockKey;
|
|
364
|
-
}
|
|
365
|
-
acquireLock(expiryMs = 5e3) {
|
|
366
|
-
const now = Date.now();
|
|
367
|
-
const lockValue = localStorage.getItem(this.key);
|
|
368
|
-
if (lockValue) {
|
|
369
|
-
const timestamp = Number(lockValue);
|
|
370
|
-
if (now - timestamp < expiryMs) {
|
|
371
|
-
return false;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
localStorage.setItem(this.key, now.toString());
|
|
375
|
-
setTimeout(() => {
|
|
376
|
-
if (this.isLocked(expiryMs)) {
|
|
377
|
-
this.releaseLock();
|
|
378
|
-
}
|
|
379
|
-
}, expiryMs);
|
|
380
|
-
return true;
|
|
381
|
-
}
|
|
382
|
-
releaseLock() {
|
|
383
|
-
localStorage.removeItem(this.key);
|
|
384
|
-
}
|
|
385
|
-
isLocked(expiryMs = 5e3) {
|
|
386
|
-
const lockValue = localStorage.getItem(this.key);
|
|
387
|
-
if (!lockValue) {
|
|
388
|
-
return false;
|
|
389
|
-
}
|
|
390
|
-
const timestamp = Number(lockValue);
|
|
391
|
-
return Date.now() - timestamp < expiryMs;
|
|
392
|
-
}
|
|
393
|
-
};
|
|
394
|
-
|
|
395
359
|
// src/control/audience.ts
|
|
360
|
+
var BATCH_SIZE = 50;
|
|
361
|
+
var BATCH_INTERVAL_MS = 5e3;
|
|
362
|
+
var MAX_EVENTS_PER_PAGE = 500;
|
|
363
|
+
var BROADCAST_CHANNEL_NAME = "levo_analytics";
|
|
396
364
|
var createLevoAudienceModule = (core, httpClient) => {
|
|
397
365
|
const _IDLE_THROTTLE = 1 * 1e3;
|
|
398
366
|
const _IDLE_TIMEOUT = 60 * 1e3;
|
|
399
367
|
let instance = null;
|
|
400
368
|
let idle_listener = null;
|
|
401
369
|
const storage_keys = {
|
|
370
|
+
/** Legacy: Lock key for identify operation (no longer used internally) */
|
|
402
371
|
identify: "lock:lv_aud_identify",
|
|
372
|
+
/** Key for tracking open tab count */
|
|
403
373
|
open_tabs: "lv_insights_ot",
|
|
374
|
+
/** Key for current tab ID (sessionStorage) */
|
|
404
375
|
current_tab: "lv_insights_ct",
|
|
405
|
-
|
|
376
|
+
/** Key for all tabs registry */
|
|
377
|
+
all_tabs: "lv_insights_at",
|
|
378
|
+
/** Device JWT for direct mode (1yr expiry, rotated on /welcome, analytics-only scope) */
|
|
379
|
+
device_token: "lv_aud_device",
|
|
380
|
+
/** Session JWT for direct mode (1yr expiry, rotated on /welcome, analytics-only scope) */
|
|
381
|
+
session_token: "lv_aud_session",
|
|
382
|
+
/** Cached insights mode ('proxy' | 'direct') */
|
|
383
|
+
mode: "lv_aud_mode"
|
|
384
|
+
};
|
|
385
|
+
let _insightsMode = "proxy";
|
|
386
|
+
let _insightsBaseUrl = "";
|
|
387
|
+
let _localStorageAvailable = true;
|
|
388
|
+
const _memoryStorage = /* @__PURE__ */ new Map();
|
|
389
|
+
const checkLocalStorageAvailable = () => {
|
|
390
|
+
if (typeof window === "undefined" || !("localStorage" in window)) {
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
try {
|
|
394
|
+
const testKey = "__levo_test__";
|
|
395
|
+
window.localStorage.setItem(testKey, "test");
|
|
396
|
+
window.localStorage.removeItem(testKey);
|
|
397
|
+
return true;
|
|
398
|
+
} catch {
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
const getFromStorage = (key) => {
|
|
403
|
+
if (typeof window === "undefined") {
|
|
404
|
+
return _memoryStorage.get(key) || null;
|
|
405
|
+
}
|
|
406
|
+
if (_localStorageAvailable) {
|
|
407
|
+
try {
|
|
408
|
+
return window.localStorage.getItem(key);
|
|
409
|
+
} catch {
|
|
410
|
+
return _memoryStorage.get(key) || null;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return _memoryStorage.get(key) || null;
|
|
414
|
+
};
|
|
415
|
+
const setInStorage = (key, value) => {
|
|
416
|
+
if (typeof window === "undefined") {
|
|
417
|
+
_memoryStorage.set(key, value);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (_localStorageAvailable) {
|
|
421
|
+
try {
|
|
422
|
+
window.localStorage.setItem(key, value);
|
|
423
|
+
return;
|
|
424
|
+
} catch {
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
_memoryStorage.set(key, value);
|
|
428
|
+
};
|
|
429
|
+
const removeFromStorage = (key) => {
|
|
430
|
+
_memoryStorage.delete(key);
|
|
431
|
+
if (typeof window === "undefined" || !_localStorageAvailable) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
try {
|
|
435
|
+
window.localStorage.removeItem(key);
|
|
436
|
+
} catch {
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
const getAuthHeaders = () => {
|
|
440
|
+
const headers = {};
|
|
441
|
+
if (_insightsMode === "direct") {
|
|
442
|
+
const deviceToken = getFromStorage(storage_keys.device_token);
|
|
443
|
+
const sessionToken = getFromStorage(storage_keys.session_token);
|
|
444
|
+
if (deviceToken) headers["Levo-Audience-Device"] = deviceToken;
|
|
445
|
+
if (sessionToken) headers["Levo-Audience-Session"] = sessionToken;
|
|
446
|
+
}
|
|
447
|
+
return headers;
|
|
448
|
+
};
|
|
449
|
+
const pingEndpoint = async (url, timeout) => {
|
|
450
|
+
let timeoutId = null;
|
|
451
|
+
try {
|
|
452
|
+
const controller = new AbortController();
|
|
453
|
+
timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
454
|
+
const response = await fetch(url, {
|
|
455
|
+
method: "GET",
|
|
456
|
+
signal: controller.signal,
|
|
457
|
+
credentials: "omit"
|
|
458
|
+
});
|
|
459
|
+
return response.ok;
|
|
460
|
+
} catch {
|
|
461
|
+
return false;
|
|
462
|
+
} finally {
|
|
463
|
+
if (timeoutId) {
|
|
464
|
+
clearTimeout(timeoutId);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
const detectInsightsMode = async () => {
|
|
469
|
+
if (core.insightsMode === "proxy") {
|
|
470
|
+
_insightsBaseUrl = core.insightsUrl;
|
|
471
|
+
return "proxy";
|
|
472
|
+
}
|
|
473
|
+
if (core.insightsMode === "direct") {
|
|
474
|
+
if (!core.directInsightsUrl) {
|
|
475
|
+
console.warn(
|
|
476
|
+
"[@levo-so/core] insightsMode='direct' but directInsightsUrl not set. Falling back to proxy."
|
|
477
|
+
);
|
|
478
|
+
_insightsBaseUrl = core.insightsUrl;
|
|
479
|
+
return "proxy";
|
|
480
|
+
}
|
|
481
|
+
_insightsBaseUrl = core.directInsightsUrl;
|
|
482
|
+
setInStorage(storage_keys.mode, "direct");
|
|
483
|
+
return "direct";
|
|
484
|
+
}
|
|
485
|
+
const cachedMode = getFromStorage(storage_keys.mode);
|
|
486
|
+
if (cachedMode === "proxy" || cachedMode === "direct") {
|
|
487
|
+
if (cachedMode === "direct" && !core.directInsightsUrl) {
|
|
488
|
+
removeFromStorage(storage_keys.mode);
|
|
489
|
+
_insightsBaseUrl = "";
|
|
490
|
+
} else {
|
|
491
|
+
const pingUrl = cachedMode === "proxy" ? `${core.insightsUrl}/ping` : `${core.directInsightsUrl}/ping`;
|
|
492
|
+
if (await pingEndpoint(pingUrl, core.pingTimeout)) {
|
|
493
|
+
_insightsBaseUrl = cachedMode === "proxy" ? core.insightsUrl : core.directInsightsUrl;
|
|
494
|
+
return cachedMode;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (await pingEndpoint(`${core.insightsUrl}/ping`, core.pingTimeout)) {
|
|
499
|
+
setInStorage(storage_keys.mode, "proxy");
|
|
500
|
+
_insightsBaseUrl = core.insightsUrl;
|
|
501
|
+
return "proxy";
|
|
502
|
+
}
|
|
503
|
+
if (core.directInsightsUrl) {
|
|
504
|
+
setInStorage(storage_keys.mode, "direct");
|
|
505
|
+
_insightsBaseUrl = core.directInsightsUrl;
|
|
506
|
+
return "direct";
|
|
507
|
+
}
|
|
508
|
+
_insightsBaseUrl = core.insightsUrl;
|
|
509
|
+
return "proxy";
|
|
406
510
|
};
|
|
407
511
|
let _session = "";
|
|
408
512
|
let _device = "";
|
|
409
513
|
let is_identified = false;
|
|
410
|
-
let
|
|
514
|
+
let _eventCount = 0;
|
|
515
|
+
let _eventQueue = [];
|
|
516
|
+
let _flushTimer = null;
|
|
517
|
+
let _channel = null;
|
|
518
|
+
let _pendingIdentify = false;
|
|
519
|
+
const buildSessionSyncMessage = () => {
|
|
520
|
+
const message = {
|
|
521
|
+
type: "SESSION_SYNC",
|
|
522
|
+
session: _session,
|
|
523
|
+
device: _device
|
|
524
|
+
};
|
|
525
|
+
if (_insightsMode === "direct") {
|
|
526
|
+
const storedDeviceToken = getFromStorage(storage_keys.device_token);
|
|
527
|
+
const storedSessionToken = getFromStorage(storage_keys.session_token);
|
|
528
|
+
if (storedDeviceToken) message.deviceToken = storedDeviceToken;
|
|
529
|
+
if (storedSessionToken) message.sessionToken = storedSessionToken;
|
|
530
|
+
}
|
|
531
|
+
return message;
|
|
532
|
+
};
|
|
533
|
+
const initBroadcastChannel = () => {
|
|
534
|
+
if (typeof BroadcastChannel === "undefined") {
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
try {
|
|
538
|
+
_channel = new BroadcastChannel(BROADCAST_CHANNEL_NAME);
|
|
539
|
+
_channel.onmessage = (event) => {
|
|
540
|
+
const { type, session, device, deviceToken, sessionToken } = event.data;
|
|
541
|
+
if (type === "SESSION_SYNC" && !is_identified) {
|
|
542
|
+
_session = session;
|
|
543
|
+
_device = device;
|
|
544
|
+
is_identified = true;
|
|
545
|
+
_pendingIdentify = false;
|
|
546
|
+
if (_insightsMode === "direct" && deviceToken && sessionToken) {
|
|
547
|
+
setInStorage(storage_keys.device_token, deviceToken);
|
|
548
|
+
setInStorage(storage_keys.session_token, sessionToken);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (type === "SESSION_REQUEST" && is_identified) {
|
|
552
|
+
_channel?.postMessage(buildSessionSyncMessage());
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
} catch (e) {
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
const requestSessionFromOtherTabs = () => {
|
|
559
|
+
_channel?.postMessage({ type: "SESSION_REQUEST" });
|
|
560
|
+
};
|
|
561
|
+
const broadcastSession = () => {
|
|
562
|
+
_channel?.postMessage(buildSessionSyncMessage());
|
|
563
|
+
};
|
|
564
|
+
const flushEventQueue = () => {
|
|
565
|
+
const baseUrl = _insightsBaseUrl || core?.insightsUrl;
|
|
566
|
+
if (_eventQueue.length === 0 || !baseUrl || !core.workspace) {
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const batch = _eventQueue.map((event) => ({
|
|
570
|
+
...event,
|
|
571
|
+
session_id: event.session_id || _session,
|
|
572
|
+
device_id: event.device_id || _device
|
|
573
|
+
}));
|
|
574
|
+
_eventQueue = [];
|
|
575
|
+
const url = `${baseUrl}/v1/insights/event/bulk?workspace=${core.workspace}`;
|
|
576
|
+
const headers = {
|
|
577
|
+
"Content-Type": "application/json",
|
|
578
|
+
...getAuthHeaders()
|
|
579
|
+
};
|
|
580
|
+
if (_insightsMode === "proxy" && navigator.sendBeacon) {
|
|
581
|
+
const success = navigator.sendBeacon(url, JSON.stringify(batch));
|
|
582
|
+
if (success) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
fetch(url, {
|
|
587
|
+
method: "POST",
|
|
588
|
+
body: JSON.stringify(batch),
|
|
589
|
+
headers,
|
|
590
|
+
keepalive: true,
|
|
591
|
+
credentials: _insightsMode === "proxy" ? "include" : "omit"
|
|
592
|
+
}).catch(() => {
|
|
593
|
+
});
|
|
594
|
+
};
|
|
595
|
+
const startFlushTimer = () => {
|
|
596
|
+
if (_flushTimer) {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
_flushTimer = setInterval(() => {
|
|
600
|
+
flushEventQueue();
|
|
601
|
+
}, BATCH_INTERVAL_MS);
|
|
602
|
+
if (typeof document !== "undefined") {
|
|
603
|
+
document.addEventListener("visibilitychange", () => {
|
|
604
|
+
if (document.visibilityState === "hidden") {
|
|
605
|
+
flushEventQueue();
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
window.addEventListener("beforeunload", () => {
|
|
609
|
+
flushEventQueue();
|
|
610
|
+
});
|
|
611
|
+
window.addEventListener("pagehide", () => {
|
|
612
|
+
flushEventQueue();
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
const stopFlushTimer = () => {
|
|
617
|
+
if (_flushTimer) {
|
|
618
|
+
clearInterval(_flushTimer);
|
|
619
|
+
_flushTimer = null;
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
const queueEvent = (collectInput) => {
|
|
623
|
+
if (_eventCount >= MAX_EVENTS_PER_PAGE) {
|
|
624
|
+
console.warn(
|
|
625
|
+
`[@levo-so/core] Event limit (${MAX_EVENTS_PER_PAGE}) reached for this page`
|
|
626
|
+
);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
_eventCount++;
|
|
630
|
+
_eventQueue.push(collectInput);
|
|
631
|
+
if (_eventQueue.length >= BATCH_SIZE) {
|
|
632
|
+
flushEventQueue();
|
|
633
|
+
}
|
|
634
|
+
};
|
|
411
635
|
const getReferrer = () => {
|
|
412
636
|
if (typeof document === "undefined") {
|
|
413
637
|
return {};
|
|
@@ -477,11 +701,13 @@ var createLevoAudienceModule = (core, httpClient) => {
|
|
|
477
701
|
const request = async (url, data) => {
|
|
478
702
|
const headers = {
|
|
479
703
|
Accept: "application/json",
|
|
480
|
-
"Content-Type": "application/json"
|
|
704
|
+
"Content-Type": "application/json",
|
|
705
|
+
...getAuthHeaders()
|
|
481
706
|
};
|
|
482
707
|
try {
|
|
483
|
-
const response = await httpClient.instance.url(core?.insightsUrl, true).url(url).options({
|
|
484
|
-
keepAlive: true
|
|
708
|
+
const response = await httpClient.instance.url(_insightsBaseUrl || core?.insightsUrl, true).url(url).options({
|
|
709
|
+
keepAlive: true,
|
|
710
|
+
credentials: _insightsMode === "proxy" ? "include" : "omit"
|
|
485
711
|
}).headers(headers).post(data).json();
|
|
486
712
|
return response;
|
|
487
713
|
} catch (error) {
|
|
@@ -493,8 +719,26 @@ var createLevoAudienceModule = (core, httpClient) => {
|
|
|
493
719
|
if (core.NODE_ENV === "development") {
|
|
494
720
|
return;
|
|
495
721
|
}
|
|
722
|
+
_eventCount = 0;
|
|
723
|
+
_localStorageAvailable = checkLocalStorageAvailable();
|
|
724
|
+
_insightsMode = await detectInsightsMode();
|
|
725
|
+
initBroadcastChannel();
|
|
726
|
+
requestSessionFromOtherTabs();
|
|
727
|
+
startFlushTimer();
|
|
728
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
496
729
|
const plugin = {
|
|
497
730
|
name: core.workspace,
|
|
731
|
+
/**
|
|
732
|
+
* Handle track events (custom events like clicks, scrolls, etc.)
|
|
733
|
+
*
|
|
734
|
+
* Enriches the event with:
|
|
735
|
+
* - Session/device IDs
|
|
736
|
+
* - Page metadata (URL, title, etc.)
|
|
737
|
+
* - Tab information
|
|
738
|
+
* - Timestamp
|
|
739
|
+
*
|
|
740
|
+
* Then queues for batched sending.
|
|
741
|
+
*/
|
|
498
742
|
track: ({ payload }) => {
|
|
499
743
|
if (!core?.insightsUrl || !core.workspace) {
|
|
500
744
|
return;
|
|
@@ -506,9 +750,11 @@ var createLevoAudienceModule = (core, httpClient) => {
|
|
|
506
750
|
event: payload.event,
|
|
507
751
|
version: payload.version || 1,
|
|
508
752
|
workspace_id: core.workspace,
|
|
753
|
+
site_id: core.site || void 0,
|
|
509
754
|
properties: {
|
|
510
755
|
...properties,
|
|
511
756
|
...payloadProperties,
|
|
757
|
+
// Remove metadata fields from properties (they're top-level)
|
|
512
758
|
version: void 0,
|
|
513
759
|
resource: void 0,
|
|
514
760
|
identifier: void 0,
|
|
@@ -528,21 +774,19 @@ var createLevoAudienceModule = (core, httpClient) => {
|
|
|
528
774
|
tab_id: getCurrentTabId(),
|
|
529
775
|
tab_count: getTabCount()
|
|
530
776
|
};
|
|
531
|
-
|
|
532
|
-
if (navigator.sendBeacon) {
|
|
533
|
-
isQueued = navigator.sendBeacon(
|
|
534
|
-
`${core?.insightsUrl}/v1/insights/event/collect`,
|
|
535
|
-
JSON.stringify(collectInput)
|
|
536
|
-
);
|
|
537
|
-
}
|
|
538
|
-
if (!isQueued) {
|
|
539
|
-
request("/v1/insights/event/collect", collectInput);
|
|
540
|
-
}
|
|
777
|
+
queueEvent(collectInput);
|
|
541
778
|
},
|
|
779
|
+
/**
|
|
780
|
+
* Handle page view events.
|
|
781
|
+
*
|
|
782
|
+
* Called when navigating to a new page (in SPAs) or on initial load.
|
|
783
|
+
* Resets the rate limit counter and tracks a page.view event.
|
|
784
|
+
*/
|
|
542
785
|
page: ({ payload }) => {
|
|
543
786
|
if (!core?.insightsUrl) {
|
|
544
787
|
return;
|
|
545
788
|
}
|
|
789
|
+
_eventCount = 0;
|
|
546
790
|
const payloadProperties = payload?.properties || {};
|
|
547
791
|
track("page.view", {
|
|
548
792
|
...properties,
|
|
@@ -552,11 +796,28 @@ var createLevoAudienceModule = (core, httpClient) => {
|
|
|
552
796
|
resource: "page"
|
|
553
797
|
});
|
|
554
798
|
},
|
|
799
|
+
/**
|
|
800
|
+
* Handle identify events (session establishment).
|
|
801
|
+
*
|
|
802
|
+
* This is called when identify() is invoked. It:
|
|
803
|
+
* 1. Checks if already identified (via BroadcastChannel) - if so, skip
|
|
804
|
+
* 2. Calls /welcome API to get session/device IDs
|
|
805
|
+
* 3. On success, broadcasts session to other tabs
|
|
806
|
+
*
|
|
807
|
+
* The /welcome API uses HttpOnly cookies to maintain persistent
|
|
808
|
+
* device identity across sessions.
|
|
809
|
+
*/
|
|
555
810
|
identify: ({ payload }) => {
|
|
556
811
|
if (!core?.insightsUrl || !core.workspace) {
|
|
557
812
|
return;
|
|
558
813
|
}
|
|
559
|
-
|
|
814
|
+
if (is_identified) {
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
if (_pendingIdentify) {
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
_pendingIdentify = true;
|
|
560
821
|
const welcomeInput = {
|
|
561
822
|
private_mode: payload.traits.private_mode,
|
|
562
823
|
dark_mode: payload.traits.dark_mode,
|
|
@@ -565,6 +826,7 @@ var createLevoAudienceModule = (core, httpClient) => {
|
|
|
565
826
|
referrer: payload.traits.referrer,
|
|
566
827
|
properties: payload.traits.properties,
|
|
567
828
|
workspace_id: core.workspace,
|
|
829
|
+
site_id: core.site || void 0,
|
|
568
830
|
hostname: window.location.hostname,
|
|
569
831
|
pathname: window.location.pathname,
|
|
570
832
|
page_title: document.title,
|
|
@@ -573,36 +835,33 @@ var createLevoAudienceModule = (core, httpClient) => {
|
|
|
573
835
|
tab_id: getCurrentTabId(),
|
|
574
836
|
tab_count: getTabCount()
|
|
575
837
|
};
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
JSON.stringify(_batch)
|
|
594
|
-
);
|
|
838
|
+
request(`/v1/insights/event/welcome`, welcomeInput).then((data) => {
|
|
839
|
+
if (is_identified) {
|
|
840
|
+
_pendingIdentify = false;
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
is_identified = true;
|
|
844
|
+
_pendingIdentify = false;
|
|
845
|
+
if (data?.content?.data?.session) {
|
|
846
|
+
_session = data?.content.data.session;
|
|
847
|
+
}
|
|
848
|
+
if (data?.content?.data?.device) {
|
|
849
|
+
_device = data?.content.data.device;
|
|
850
|
+
}
|
|
851
|
+
if (_insightsMode === "direct" && data?.content?.meta) {
|
|
852
|
+
const meta = data.content.meta;
|
|
853
|
+
if (meta.device_token) {
|
|
854
|
+
setInStorage(storage_keys.device_token, meta.device_token);
|
|
595
855
|
}
|
|
596
|
-
if (
|
|
597
|
-
|
|
856
|
+
if (meta.session_token) {
|
|
857
|
+
setInStorage(storage_keys.session_token, meta.session_token);
|
|
598
858
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
}
|
|
859
|
+
}
|
|
860
|
+
broadcastSession();
|
|
861
|
+
}).catch((e) => {
|
|
862
|
+
_pendingIdentify = false;
|
|
863
|
+
return getLevoError(e);
|
|
864
|
+
});
|
|
606
865
|
}
|
|
607
866
|
};
|
|
608
867
|
instance = Analytics({
|
|
@@ -650,10 +909,10 @@ var createLevoAudienceModule = (core, httpClient) => {
|
|
|
650
909
|
if (!core.workspace) {
|
|
651
910
|
return;
|
|
652
911
|
}
|
|
653
|
-
if (instance
|
|
912
|
+
if (instance) {
|
|
654
913
|
return instance.track(event, properties);
|
|
655
914
|
}
|
|
656
|
-
|
|
915
|
+
const collectInput = {
|
|
657
916
|
event,
|
|
658
917
|
version: properties?.version || 1,
|
|
659
918
|
properties: {
|
|
@@ -674,23 +933,39 @@ var createLevoAudienceModule = (core, httpClient) => {
|
|
|
674
933
|
resource: properties?.resource || "page",
|
|
675
934
|
identifier: properties?.identifier || properties?.id,
|
|
676
935
|
workspace_id: core.workspace,
|
|
936
|
+
site_id: core.site || void 0,
|
|
677
937
|
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
678
938
|
tab_id: getCurrentTabId(),
|
|
679
939
|
tab_count: getTabCount()
|
|
680
|
-
}
|
|
940
|
+
};
|
|
941
|
+
queueEvent(collectInput);
|
|
942
|
+
};
|
|
943
|
+
const destroy = () => {
|
|
944
|
+
flushEventQueue();
|
|
945
|
+
stopFlushTimer();
|
|
946
|
+
_channel?.close();
|
|
681
947
|
};
|
|
682
948
|
return {
|
|
949
|
+
/** Get the underlying analytics instance (read-only) */
|
|
683
950
|
get instance() {
|
|
684
951
|
return instance;
|
|
685
952
|
},
|
|
953
|
+
/** Check if session has been established (read-only) */
|
|
686
954
|
get is_identified() {
|
|
687
955
|
return is_identified;
|
|
688
956
|
},
|
|
957
|
+
/** Storage keys used by the module */
|
|
689
958
|
storage_keys,
|
|
959
|
+
/** Initialize the module for a page */
|
|
690
960
|
initiate,
|
|
961
|
+
/** Identify/establish session */
|
|
691
962
|
identify,
|
|
963
|
+
/** Track a custom event */
|
|
692
964
|
track,
|
|
693
|
-
bounce
|
|
965
|
+
/** Track a bounce event */
|
|
966
|
+
bounce,
|
|
967
|
+
/** Cleanup the module */
|
|
968
|
+
destroy
|
|
694
969
|
};
|
|
695
970
|
};
|
|
696
971
|
|
|
@@ -700,11 +975,16 @@ var createLevoControl = (options) => {
|
|
|
700
975
|
const {
|
|
701
976
|
appName,
|
|
702
977
|
workspace = null,
|
|
978
|
+
site = null,
|
|
703
979
|
apiUrl = "https://public-api.levo.so",
|
|
704
980
|
insightsUrl = "/.levo/insights/api",
|
|
981
|
+
directInsightsUrl,
|
|
982
|
+
insightsMode = "auto",
|
|
983
|
+
pingTimeout = 3e3,
|
|
705
984
|
NODE_ENV = "",
|
|
706
985
|
APP_MODE = "production"
|
|
707
986
|
} = options;
|
|
987
|
+
const sanitizedPingTimeout = Number.isFinite(pingTimeout) && pingTimeout >= 0 ? pingTimeout : 3e3;
|
|
708
988
|
if (workspace) {
|
|
709
989
|
if (!workspaceIdRegex.test(workspace)) {
|
|
710
990
|
throw new Error(
|
|
@@ -713,6 +993,7 @@ var createLevoControl = (options) => {
|
|
|
713
993
|
}
|
|
714
994
|
}
|
|
715
995
|
let _workspace = workspace;
|
|
996
|
+
let _site = site;
|
|
716
997
|
return {
|
|
717
998
|
get workspace() {
|
|
718
999
|
return _workspace;
|
|
@@ -727,6 +1008,12 @@ var createLevoControl = (options) => {
|
|
|
727
1008
|
}
|
|
728
1009
|
_workspace = value;
|
|
729
1010
|
},
|
|
1011
|
+
get site() {
|
|
1012
|
+
return _site;
|
|
1013
|
+
},
|
|
1014
|
+
set site(value) {
|
|
1015
|
+
_site = value;
|
|
1016
|
+
},
|
|
730
1017
|
get appName() {
|
|
731
1018
|
return appName;
|
|
732
1019
|
},
|
|
@@ -736,6 +1023,15 @@ var createLevoControl = (options) => {
|
|
|
736
1023
|
get insightsUrl() {
|
|
737
1024
|
return insightsUrl;
|
|
738
1025
|
},
|
|
1026
|
+
get directInsightsUrl() {
|
|
1027
|
+
return directInsightsUrl || "";
|
|
1028
|
+
},
|
|
1029
|
+
get insightsMode() {
|
|
1030
|
+
return insightsMode;
|
|
1031
|
+
},
|
|
1032
|
+
get pingTimeout() {
|
|
1033
|
+
return sanitizedPingTimeout;
|
|
1034
|
+
},
|
|
739
1035
|
get NODE_ENV() {
|
|
740
1036
|
return NODE_ENV;
|
|
741
1037
|
},
|
|
@@ -1295,16 +1591,30 @@ var AnalyticsEventsList = [
|
|
|
1295
1591
|
"page.impression",
|
|
1296
1592
|
"page.scroll",
|
|
1297
1593
|
"page.view",
|
|
1298
|
-
"page.metrics",
|
|
1299
1594
|
"page.selection",
|
|
1300
|
-
"page.speedtest",
|
|
1301
1595
|
// User Activity Events
|
|
1302
1596
|
"user.active",
|
|
1303
|
-
"user.speedtest",
|
|
1304
1597
|
"user.idle"
|
|
1305
1598
|
];
|
|
1306
1599
|
var AnalyticsEvents = listToRecord(AnalyticsEventsList);
|
|
1307
1600
|
|
|
1601
|
+
// src/types/forum/post.ts
|
|
1602
|
+
var FORUM_POST_KIND = {
|
|
1603
|
+
post: "post",
|
|
1604
|
+
poll: "poll",
|
|
1605
|
+
gallery: "gallery",
|
|
1606
|
+
link: "link"
|
|
1607
|
+
};
|
|
1608
|
+
var ForumPostKindExpandedList = Object.keys(FORUM_POST_KIND);
|
|
1609
|
+
|
|
1610
|
+
// src/types/forum/comment.ts
|
|
1611
|
+
var COMMENT_STATUS = {
|
|
1612
|
+
active: "active",
|
|
1613
|
+
edited: "edited",
|
|
1614
|
+
deleted: "deleted",
|
|
1615
|
+
pending_moderation: "pending_moderation"
|
|
1616
|
+
};
|
|
1617
|
+
|
|
1308
1618
|
// src/constants/collection/fieldInterfaceFormat.ts
|
|
1309
1619
|
var formatsByInterface = {
|
|
1310
1620
|
ArrayWidget: [],
|
|
@@ -1350,7 +1660,6 @@ var defaultByKinds = {
|
|
|
1350
1660
|
record: null,
|
|
1351
1661
|
group: [],
|
|
1352
1662
|
collection: {
|
|
1353
|
-
o2m: null,
|
|
1354
1663
|
m2o: null,
|
|
1355
1664
|
o2o: null,
|
|
1356
1665
|
m2m: []
|
|
@@ -1515,6 +1824,7 @@ var LEVO_INTEGRATIONS = {
|
|
|
1515
1824
|
event: "so.levo.event",
|
|
1516
1825
|
membership: "so.levo.membership",
|
|
1517
1826
|
payment: "so.levo.payment",
|
|
1827
|
+
community: "so.levo.community",
|
|
1518
1828
|
search_console: "com.google.search_console"
|
|
1519
1829
|
};
|
|
1520
1830
|
var LEVO_INTEGRATIONS_LIST = Object.values(LEVO_INTEGRATIONS);
|
|
@@ -1530,4 +1840,40 @@ var formatImagePath = (string) => {
|
|
|
1530
1840
|
return "";
|
|
1531
1841
|
};
|
|
1532
1842
|
|
|
1533
|
-
|
|
1843
|
+
// src/utils/lock.ts
|
|
1844
|
+
var LocalStorageLock = class {
|
|
1845
|
+
key;
|
|
1846
|
+
constructor(lockKey) {
|
|
1847
|
+
this.key = lockKey;
|
|
1848
|
+
}
|
|
1849
|
+
acquireLock(expiryMs = 5e3) {
|
|
1850
|
+
const now = Date.now();
|
|
1851
|
+
const lockValue = localStorage.getItem(this.key);
|
|
1852
|
+
if (lockValue) {
|
|
1853
|
+
const timestamp = Number(lockValue);
|
|
1854
|
+
if (now - timestamp < expiryMs) {
|
|
1855
|
+
return false;
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
localStorage.setItem(this.key, now.toString());
|
|
1859
|
+
setTimeout(() => {
|
|
1860
|
+
if (this.isLocked(expiryMs)) {
|
|
1861
|
+
this.releaseLock();
|
|
1862
|
+
}
|
|
1863
|
+
}, expiryMs);
|
|
1864
|
+
return true;
|
|
1865
|
+
}
|
|
1866
|
+
releaseLock() {
|
|
1867
|
+
localStorage.removeItem(this.key);
|
|
1868
|
+
}
|
|
1869
|
+
isLocked(expiryMs = 5e3) {
|
|
1870
|
+
const lockValue = localStorage.getItem(this.key);
|
|
1871
|
+
if (!lockValue) {
|
|
1872
|
+
return false;
|
|
1873
|
+
}
|
|
1874
|
+
const timestamp = Number(lockValue);
|
|
1875
|
+
return Date.now() - timestamp < expiryMs;
|
|
1876
|
+
}
|
|
1877
|
+
};
|
|
1878
|
+
|
|
1879
|
+
export { AnalyticsEvents, AnalyticsEventsList, COMMENT_STATUS, CommonFieldIntefaces, CommonFieldInterfacesList, ComplexFieldInterfacesList, FORUM_POST_KIND, FieldInterfaces, FieldInterfacesList, FieldKind, FieldKindList, ForumPostKindExpandedList, LEVO_INTEGRATIONS, LEVO_INTEGRATIONS_LIST, LevoError, LevoOAuthProviderList, LocalStorageLock, MediaKind, MediaKindList, createLevoClient, defaultByKinds, formatImagePath, formatsByInterface, getLevoError, interfaceKinds };
|