@levo-so/insights 0.1.61

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.
@@ -0,0 +1,305 @@
1
+ import { ILevoClient } from '@levo-so/core';
2
+ import { AnalyticsInstance } from 'analytics';
3
+
4
+ declare const AnalyticsEventsList: readonly ["bevy.collection.filled", "bevy.collection.submitted", "bevy.collection.view", "block.view", "blog.post.view", "event.booking.confirmed", "event.booking.initiated", "event.booking.pending", "event.coupon.applied", "event.event.view", "form.change", "form.submit", "membership.account.signin", "membership.account.signout", "membership.account.signup", "button.click", "page.bounce", "page.click", "page.copy", "page.impression", "page.scroll", "page.view", "page.selection", "user.active", "user.idle"];
5
+ type AnalyticsEventType = (typeof AnalyticsEventsList)[number];
6
+ declare const AnalyticsEvents: {
7
+ "bevy.collection.filled": "bevy.collection.filled";
8
+ "bevy.collection.submitted": "bevy.collection.submitted";
9
+ "bevy.collection.view": "bevy.collection.view";
10
+ "block.view": "block.view";
11
+ "blog.post.view": "blog.post.view";
12
+ "event.booking.confirmed": "event.booking.confirmed";
13
+ "event.booking.initiated": "event.booking.initiated";
14
+ "event.booking.pending": "event.booking.pending";
15
+ "event.coupon.applied": "event.coupon.applied";
16
+ "event.event.view": "event.event.view";
17
+ "form.change": "form.change";
18
+ "form.submit": "form.submit";
19
+ "membership.account.signin": "membership.account.signin";
20
+ "membership.account.signout": "membership.account.signout";
21
+ "membership.account.signup": "membership.account.signup";
22
+ "button.click": "button.click";
23
+ "page.bounce": "page.bounce";
24
+ "page.click": "page.click";
25
+ "page.copy": "page.copy";
26
+ "page.impression": "page.impression";
27
+ "page.scroll": "page.scroll";
28
+ "page.view": "page.view";
29
+ "page.selection": "page.selection";
30
+ "user.active": "user.active";
31
+ "user.idle": "user.idle";
32
+ };
33
+ declare namespace ILevoAudience {
34
+ interface Traits {
35
+ private_mode: boolean;
36
+ dark_mode: boolean;
37
+ locale: string;
38
+ timezone: string;
39
+ referrer: {
40
+ type: string;
41
+ referrer: Record<string, string>;
42
+ data: Record<string, string>;
43
+ };
44
+ properties: Record<string, any>;
45
+ withLock?: boolean;
46
+ }
47
+ type BaseProperties = Traits & {
48
+ resource: string;
49
+ identifier: string;
50
+ };
51
+ interface BlockProperties {
52
+ id: string;
53
+ }
54
+ interface ActivityProperties {
55
+ seconds_idle?: number;
56
+ seconds_active?: number;
57
+ }
58
+ interface ScrollProperties {
59
+ percent: number;
60
+ }
61
+ type Properties = Partial<BaseProperties & BlockProperties & ActivityProperties & ScrollProperties> & Partial<Omit<BaseInput, "workspace_id" | "tab_count" | "tab_id">> & Record<string, any>;
62
+ type EventProperties = {
63
+ properties: Properties;
64
+ traits: Traits;
65
+ event: AnalyticsEventType;
66
+ type: "track" | "page" | "identify";
67
+ userId: string | null;
68
+ };
69
+ type BaseInput = {
70
+ workspace_id: string;
71
+ site_id?: string;
72
+ hostname: string;
73
+ pathname: string;
74
+ page_title: string;
75
+ url: string;
76
+ created_at: string;
77
+ tab_id: string;
78
+ tab_count: number;
79
+ };
80
+ type WelcomeInput = Traits & BaseInput;
81
+ type WelcomeResponse = {
82
+ session: string;
83
+ device: string;
84
+ };
85
+ type CollectInput = BaseInput & {
86
+ session_id?: string;
87
+ device_id?: string;
88
+ event: AnalyticsEventType;
89
+ version?: number;
90
+ properties: Properties;
91
+ resource: string;
92
+ identifier: string | undefined;
93
+ };
94
+ type ActivityStatus = {
95
+ isIdle: boolean;
96
+ isDisabled: boolean;
97
+ active: number;
98
+ idle: number;
99
+ };
100
+ }
101
+
102
+ interface IInsightOptions {
103
+ /** Site ID for analytics tracking */
104
+ site?: string | null;
105
+ insightsUrl?: string;
106
+ /** Direct insights API URL for third-party mode (when reverse proxy unavailable) */
107
+ directInsightsUrl?: string;
108
+ /** Insights mode: 'auto' (default) detects via ping, 'proxy' forces reverse proxy, 'direct' forces direct API */
109
+ insightsMode?: "auto" | "proxy" | "direct";
110
+ /** Timeout for proxy ping detection in ms (default: 3000) */
111
+ pingTimeout?: number;
112
+ }
113
+
114
+ /**
115
+ * @fileoverview Levo Audience Analytics Module
116
+ *
117
+ * This module provides client-side analytics tracking for Levo-powered websites.
118
+ * It handles event collection, batching, cross-tab synchronization, and reliable
119
+ * delivery of analytics data to the Levo Insights API.
120
+ *
121
+ * ## Architecture Overview
122
+ *
123
+ * ```
124
+ * ┌────────────────────────────────────────────────────────────────────────┐
125
+ * │ Browser Tab │
126
+ * │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │
127
+ * │ │ track() │───▶│ EventQueue │───▶│ /v1/insights/event/bulk │ │
128
+ * │ │ page() │ │ (batching) │ │ (sendBeacon / fetch) │ │
129
+ * │ │ identify() │ └──────────────┘ └──────────────────────────┘ │
130
+ * │ └──────────────┘ │ │
131
+ * │ │ │ flush on: │
132
+ * │ │ │ - 50 events (BATCH_SIZE) │
133
+ * │ │ │ - 5s interval (BATCH_INTERVAL_MS) │
134
+ * │ │ │ - page exit (visibilitychange) │
135
+ * │ │ │ - beforeunload / pagehide │
136
+ * │ ▼ │ │
137
+ * │ ┌──────────────┐ │ │
138
+ * │ │ Rate Limiter │───────────┘ │
139
+ * │ │ (500/page) │ │
140
+ * │ └──────────────┘ │
141
+ * └────────────────────────────────────────────────────────────────────────┘
142
+ * │
143
+ * │ BroadcastChannel
144
+ * │ (SESSION_SYNC, SESSION_REQUEST)
145
+ * ▼
146
+ * ┌────────────────────────────────────────────────────────────────────────┐
147
+ * │ Other Browser Tabs │
148
+ * │ - Receive session/device IDs from first tab │
149
+ * │ - Skip /welcome API call if session already established │
150
+ * └────────────────────────────────────────────────────────────────────────┘
151
+ * ```
152
+ *
153
+ * ## Key Features
154
+ *
155
+ * ### 1. Event Batching
156
+ * Instead of sending each event immediately (which caused 100+ requests per page),
157
+ * events are queued and sent in batches. This reduces:
158
+ * - Network requests by 50-100x
159
+ * - Server load
160
+ * - Bandwidth usage
161
+ *
162
+ * ### 2. Cross-Tab Session Synchronization
163
+ * Uses BroadcastChannel API to share session/device IDs across tabs:
164
+ * - First tab to complete /welcome broadcasts session to others
165
+ * - Other tabs skip /welcome API call, reducing duplicate sessions
166
+ * - Solves "Open in New Tab" race condition where multiple tabs would
167
+ * each create separate sessions
168
+ *
169
+ * ### 3. Reliable Page Exit Handling
170
+ * Uses multiple mechanisms to ensure events are sent before page unload:
171
+ * - visibilitychange (most reliable, fires when tab hidden)
172
+ * - beforeunload (backup for navigation)
173
+ * - pagehide (backup for bfcache scenarios)
174
+ * - sendBeacon API (survives page unload)
175
+ *
176
+ * ### 4. Rate Limiting
177
+ * Prevents runaway loops or malicious spam:
178
+ * - Max 500 events per page
179
+ * - Counter resets on page navigation
180
+ *
181
+ * ## Session Flow
182
+ *
183
+ * ```
184
+ * Page Load
185
+ * │
186
+ * ▼
187
+ * initiate() called
188
+ * │
189
+ * ├── Initialize BroadcastChannel
190
+ * │
191
+ * ├── Request session from other tabs (SESSION_REQUEST)
192
+ * │
193
+ * ├── Wait 50ms for response
194
+ * │ │
195
+ * │ ├── If SESSION_SYNC received: Use shared session, skip /welcome
196
+ * │ │
197
+ * │ └── If no response: Call /welcome API
198
+ * │ │
199
+ * │ └── On success: Broadcast SESSION_SYNC to other tabs
200
+ * │
201
+ * └── Start tracking events
202
+ * ```
203
+ *
204
+ * ## API Endpoints Used
205
+ *
206
+ * - `POST /v1/insights/event/welcome` - Initial session establishment
207
+ * - Called once per session (first tab only, thanks to BroadcastChannel)
208
+ * - Returns session_id and device_id from HttpOnly cookies
209
+ *
210
+ * - `POST /v1/insights/event/bulk` - Batch event submission
211
+ * - Receives array of CollectInput events
212
+ * - Used for all event tracking (replaces individual /collect calls)
213
+ *
214
+ * ## Configuration Constants
215
+ *
216
+ * - BATCH_SIZE (50): Flush queue when this many events accumulated
217
+ * - BATCH_INTERVAL_MS (5000): Flush queue every 5 seconds regardless of size
218
+ * - MAX_EVENTS_PER_PAGE (500): Rate limit per page to prevent abuse
219
+ * - BROADCAST_CHANNEL_NAME ("levo_analytics"): Channel for cross-tab sync
220
+ *
221
+ * ## Browser Compatibility
222
+ *
223
+ * - BroadcastChannel: Modern browsers (Chrome 54+, Firefox 38+, Safari 15.4+)
224
+ * - Gracefully degrades: If unavailable, each tab calls /welcome independently
225
+ *
226
+ * - sendBeacon: Universal support (Chrome 39+, Firefox 31+, Safari 11.1+)
227
+ * - Fallback: fetch() with keepalive option
228
+ *
229
+ * @module @levo-so/core/control/audience
230
+ */
231
+
232
+ /**
233
+ * Creates the Levo Audience analytics module.
234
+ *
235
+ * This factory function creates a new audience module instance tied to
236
+ * a specific Levo control instance and HTTP client. The module manages
237
+ * all analytics tracking for the page.
238
+ *
239
+ * @param core - The Levo control instance containing workspace config
240
+ * @param httpClient - HTTP client for making API requests
241
+ * @returns The audience module with tracking methods
242
+ *
243
+ * @example
244
+ * ```typescript
245
+ * const audience = createLevoAudienceModule(levoControl, httpClient);
246
+ *
247
+ * // Initialize on page load
248
+ * await audience.initiate({ id: 'page-123' });
249
+ *
250
+ * // Track custom events
251
+ * audience.track('button.click', { buttonId: 'cta-signup' });
252
+ *
253
+ * // Cleanup on unmount (optional, for SPAs)
254
+ * audience.destroy();
255
+ * ```
256
+ */
257
+ declare const createLevoInsights: (client: ILevoClient, options?: IInsightOptions) => {
258
+ readonly insightsUrl: string;
259
+ /** Get the underlying analytics instance (read-only) */
260
+ readonly instance: AnalyticsInstance | null;
261
+ /** Check if session has been established (read-only) */
262
+ readonly is_identified: boolean;
263
+ /** Storage keys used by the module */
264
+ storage_keys: {
265
+ /** Legacy: Lock key for identify operation (no longer used internally) */
266
+ identify: string;
267
+ /** Key for tracking open tab count */
268
+ open_tabs: string;
269
+ /** Key for current tab ID (sessionStorage) */
270
+ current_tab: string;
271
+ /** Key for all tabs registry */
272
+ all_tabs: string;
273
+ /** Device JWT for direct mode (1yr expiry, rotated on /welcome, analytics-only scope) */
274
+ device_token: string;
275
+ /** Session JWT for direct mode (1yr expiry, rotated on /welcome, analytics-only scope) */
276
+ session_token: string;
277
+ /** Cached insights mode ('proxy' | 'direct') */
278
+ mode: string;
279
+ };
280
+ /** Initialize the module for a page */
281
+ initiate: (properties: ILevoAudience.Properties) => Promise<any>;
282
+ /** Identify/establish session */
283
+ identify: (options?: {
284
+ withLock?: boolean;
285
+ }) => Promise<void>;
286
+ /** Track a custom event */
287
+ track: <T extends ILevoAudience.Properties>(event: AnalyticsEventType, properties: T & ILevoAudience.Properties) => Promise<any>;
288
+ /** Track a bounce event */
289
+ bounce: (properties: ILevoAudience.Properties) => Promise<void>;
290
+ /** Cleanup the module */
291
+ destroy: () => void;
292
+ };
293
+ /**
294
+ * Type representing the audience module instance.
295
+ * Use this when you need to type a variable holding the module.
296
+ *
297
+ * @example
298
+ * ```typescript
299
+ * let audience: ILevoAudienceModule;
300
+ * audience = createLevoAudienceModule(core, httpClient);
301
+ * ```
302
+ */
303
+ type ILevoInsights = ReturnType<typeof createLevoInsights>;
304
+
305
+ export { type AnalyticsEventType, AnalyticsEvents, AnalyticsEventsList, type IInsightOptions, ILevoAudience, type ILevoInsights, createLevoInsights };
package/dist/index.js ADDED
@@ -0,0 +1,941 @@
1
+ import { onUserActivity } from '@analytics/activity-utils';
2
+ import { getVisitorSource } from '@analytics/visitor-source';
3
+ import { listToRecord, getLevoError } from '@levo-so/core';
4
+ import { Analytics } from 'analytics';
5
+ import getUserLocale from 'get-user-locale';
6
+
7
+ // src/index.ts
8
+
9
+ // src/utils/isIncognito.ts
10
+ var isIncognito = () => new Promise((resolve, reject) => {
11
+ let browserName = "Unknown";
12
+ function __callback(isPrivate) {
13
+ resolve({
14
+ isPrivate,
15
+ browserName
16
+ });
17
+ }
18
+ function identifyChromium() {
19
+ const ua = navigator.userAgent;
20
+ if (ua.match(/Chrome/)) {
21
+ if (navigator.brave !== void 0) {
22
+ return "Brave";
23
+ }
24
+ if (ua.match(/Edg/)) {
25
+ return "Edge";
26
+ }
27
+ if (ua.match(/OPR/)) {
28
+ return "Opera";
29
+ }
30
+ return "Chrome";
31
+ }
32
+ return "Chromium";
33
+ }
34
+ function assertEvalToString(value) {
35
+ return value === eval.toString().length;
36
+ }
37
+ function isSafari() {
38
+ const v = navigator.vendor;
39
+ return v !== void 0 && v.indexOf("Apple") === 0 && assertEvalToString(37);
40
+ }
41
+ function isChrome() {
42
+ const v = navigator.vendor;
43
+ return v !== void 0 && v.indexOf("Google") === 0 && assertEvalToString(33);
44
+ }
45
+ function isFirefox() {
46
+ return document.documentElement !== void 0 && document.documentElement.style.MozAppearance !== void 0 && assertEvalToString(37);
47
+ }
48
+ function isMSIE() {
49
+ return navigator.msSaveBlob !== void 0 && assertEvalToString(39);
50
+ }
51
+ function newSafariTest() {
52
+ const tmp_name = String(Math.random());
53
+ try {
54
+ const db = window.indexedDB.open(tmp_name, 1);
55
+ db.onupgradeneeded = (i) => {
56
+ const res = i.target?.result;
57
+ try {
58
+ res.createObjectStore("test", {
59
+ autoIncrement: true
60
+ }).put(new Blob());
61
+ __callback(false);
62
+ } catch (e) {
63
+ let message = e;
64
+ if (e instanceof Error) {
65
+ message = e.message ?? e;
66
+ }
67
+ if (typeof message !== "string") {
68
+ return __callback(false);
69
+ }
70
+ const matchesExpectedError = /BlobURLs are not yet supported/.test(message);
71
+ return __callback(matchesExpectedError);
72
+ } finally {
73
+ res.close();
74
+ window.indexedDB.deleteDatabase(tmp_name);
75
+ }
76
+ };
77
+ } catch (e) {
78
+ return __callback(false);
79
+ }
80
+ }
81
+ function oldSafariTest() {
82
+ const openDB = window.openDatabase;
83
+ const storage = window.localStorage;
84
+ try {
85
+ openDB(null, null, null, null);
86
+ } catch (e) {
87
+ return __callback(true);
88
+ }
89
+ try {
90
+ storage.setItem("test", "1");
91
+ storage.removeItem("test");
92
+ } catch (e) {
93
+ return __callback(true);
94
+ }
95
+ return __callback(false);
96
+ }
97
+ function safariPrivateTest() {
98
+ if (navigator.maxTouchPoints !== void 0) {
99
+ newSafariTest();
100
+ } else {
101
+ oldSafariTest();
102
+ }
103
+ }
104
+ function getQuotaLimit() {
105
+ const w = window;
106
+ if (w.performance !== void 0 && w.performance.memory !== void 0 && w.performance.memory.jsHeapSizeLimit !== void 0) {
107
+ return performance.memory.jsHeapSizeLimit;
108
+ }
109
+ return 1073741824;
110
+ }
111
+ function storageQuotaChromePrivateTest() {
112
+ navigator.webkitTemporaryStorage.queryUsageAndQuota(
113
+ (_, quota) => {
114
+ const quotaInMib = Math.round(quota / (1024 * 1024));
115
+ const quotaLimitInMib = Math.round(getQuotaLimit() / (1024 * 1024)) * 2;
116
+ __callback(quotaInMib < quotaLimitInMib);
117
+ },
118
+ (e) => {
119
+ reject(new Error(`detectIncognito somehow failed to query storage quota: ${e.message}`));
120
+ }
121
+ );
122
+ }
123
+ function oldChromePrivateTest() {
124
+ const fs = window.webkitRequestFileSystem;
125
+ const success = () => {
126
+ __callback(false);
127
+ };
128
+ const error = () => {
129
+ __callback(true);
130
+ };
131
+ fs(0, 1, success, error);
132
+ }
133
+ function chromePrivateTest() {
134
+ if (self.Promise !== void 0 && self.Promise.allSettled !== void 0) {
135
+ storageQuotaChromePrivateTest();
136
+ } else {
137
+ oldChromePrivateTest();
138
+ }
139
+ }
140
+ function firefoxPrivateTest() {
141
+ __callback(navigator.serviceWorker === void 0);
142
+ }
143
+ function msiePrivateTest() {
144
+ __callback(window.indexedDB === void 0);
145
+ }
146
+ function main() {
147
+ if (isSafari()) {
148
+ browserName = "Safari";
149
+ safariPrivateTest();
150
+ } else if (isChrome()) {
151
+ browserName = identifyChromium();
152
+ chromePrivateTest();
153
+ } else if (isFirefox()) {
154
+ browserName = "Firefox";
155
+ firefoxPrivateTest();
156
+ } else if (isMSIE()) {
157
+ browserName = "Internet Explorer";
158
+ msiePrivateTest();
159
+ } else {
160
+ reject(new Error("detectIncognito cannot determine the browser"));
161
+ }
162
+ }
163
+ main();
164
+ });
165
+ var isIncognito_default = isIncognito;
166
+
167
+ // src/utils/getUserProperties.ts
168
+ var getGPUInfo = () => {
169
+ const canvas = document.createElement("canvas");
170
+ const gl = canvas?.getContext("webgl") || canvas?.getContext("experimental-webgl");
171
+ if (!gl) {
172
+ return null;
173
+ }
174
+ const debugInfo = gl?.getExtension("WEBGL_debug_renderer_info");
175
+ return debugInfo ? gl?.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) : null;
176
+ };
177
+ var getUserProperties = async () => {
178
+ let locale = "";
179
+ let timezone = "";
180
+ let dark_mode = false;
181
+ let private_mode = false;
182
+ const properties = {};
183
+ if (!window) {
184
+ return {
185
+ locale,
186
+ timezone,
187
+ dark_mode,
188
+ private_mode,
189
+ properties
190
+ };
191
+ }
192
+ try {
193
+ ({ isPrivate: private_mode } = await isIncognito_default());
194
+ } catch (e) {
195
+ console.error(e);
196
+ }
197
+ try {
198
+ dark_mode = window.matchMedia?.("(prefers-color-scheme: dark)").matches;
199
+ } catch (e) {
200
+ console.error(e);
201
+ }
202
+ try {
203
+ timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
204
+ } catch (e) {
205
+ console.error(e);
206
+ }
207
+ try {
208
+ locale = getUserLocale();
209
+ } catch (e) {
210
+ console.error(e);
211
+ }
212
+ if (navigator && "connection" in navigator) {
213
+ const connection = navigator.connection || navigator?.mozConnection || navigator.webkitConnection;
214
+ properties.network_type = connection.effectiveType;
215
+ }
216
+ properties.width = window?.innerWidth;
217
+ properties.height = window?.innerHeight;
218
+ properties.pixel_ratio = window?.devicePixelRatio;
219
+ if (screen) {
220
+ properties.screen_width = screen?.width;
221
+ properties.screen_height = screen?.height;
222
+ properties.color_depth = screen?.colorDepth;
223
+ properties.orientation = screen?.orientation?.type;
224
+ }
225
+ if (navigator && "getBattery" in navigator) {
226
+ const battery = await navigator?.getBattery();
227
+ if (battery) {
228
+ properties.battery_level = Number((battery?.level * 100).toFixed(0));
229
+ properties.battery_charging = battery?.charging;
230
+ properties.battery_discharging_time = battery?.dischargingTime;
231
+ properties.battery_full_charge_capacity = battery?.fullChargeCapacity;
232
+ properties.battery_charging_time = battery?.chargingTime;
233
+ }
234
+ }
235
+ if (navigator && "deviceMemory" in navigator) {
236
+ properties.device_memory = navigator?.deviceMemory;
237
+ }
238
+ const supportsTouch = "ontouchstart" in window || typeof navigator?.maxTouchPoints !== "undefined" && navigator?.maxTouchPoints > 0;
239
+ properties.touch_supported = supportsTouch;
240
+ if ("hardwareConcurrency" in navigator) {
241
+ properties.hardware_concurrency = navigator?.hardwareConcurrency;
242
+ }
243
+ if (navigator && "mediaDevices" in navigator) {
244
+ const mediaDevices = navigator?.mediaDevices;
245
+ const devices = await mediaDevices?.enumerateDevices();
246
+ const hasCamera = devices.some((device) => device?.kind === "videoinput");
247
+ properties.camera_supported = hasCamera;
248
+ const hasMicrophone = devices.some((device) => device?.kind === "audioinput");
249
+ properties.microphone_supported = hasMicrophone;
250
+ }
251
+ properties.gpu = getGPUInfo();
252
+ return {
253
+ locale,
254
+ timezone,
255
+ dark_mode,
256
+ private_mode,
257
+ properties
258
+ };
259
+ };
260
+ var AnalyticsEventsList = [
261
+ // Bevy Events
262
+ "bevy.collection.filled",
263
+ "bevy.collection.submitted",
264
+ "bevy.collection.view",
265
+ // Block Events
266
+ "block.view",
267
+ // Blog Events
268
+ "blog.post.view",
269
+ // Event Events
270
+ "event.booking.confirmed",
271
+ "event.booking.initiated",
272
+ "event.booking.pending",
273
+ "event.coupon.applied",
274
+ "event.event.view",
275
+ // Form Events
276
+ "form.change",
277
+ "form.submit",
278
+ // Membership Events
279
+ "membership.account.signin",
280
+ "membership.account.signout",
281
+ "membership.account.signup",
282
+ // UI Events
283
+ "button.click",
284
+ "page.bounce",
285
+ "page.click",
286
+ "page.copy",
287
+ "page.impression",
288
+ "page.scroll",
289
+ "page.view",
290
+ "page.selection",
291
+ // User Activity Events
292
+ "user.active",
293
+ "user.idle"
294
+ ];
295
+ var AnalyticsEvents = listToRecord(AnalyticsEventsList);
296
+
297
+ // src/index.ts
298
+ var BATCH_SIZE = 50;
299
+ var BATCH_INTERVAL_MS = 5e3;
300
+ var MAX_EVENTS_PER_PAGE = 500;
301
+ var BROADCAST_CHANNEL_NAME = "levo_analytics";
302
+ var createLevoInsights = (client, options = {}) => {
303
+ const { workspace, fetch: levoFetch, NODE_ENV } = client;
304
+ const {
305
+ insightsMode = "auto",
306
+ insightsUrl = "",
307
+ directInsightsUrl = "",
308
+ pingTimeout: providedPingTimeout = 3e3,
309
+ site
310
+ } = options;
311
+ const pingTimeout = Number.isFinite(providedPingTimeout) && providedPingTimeout >= 0 ? providedPingTimeout : 3e3;
312
+ const _IDLE_THROTTLE = 1 * 1e3;
313
+ const _IDLE_TIMEOUT = 60 * 1e3;
314
+ let instance = null;
315
+ let idle_listener = null;
316
+ const storage_keys = {
317
+ /** Legacy: Lock key for identify operation (no longer used internally) */
318
+ identify: "lock:lv_aud_identify",
319
+ /** Key for tracking open tab count */
320
+ open_tabs: "lv_insights_ot",
321
+ /** Key for current tab ID (sessionStorage) */
322
+ current_tab: "lv_insights_ct",
323
+ /** Key for all tabs registry */
324
+ all_tabs: "lv_insights_at",
325
+ /** Device JWT for direct mode (1yr expiry, rotated on /welcome, analytics-only scope) */
326
+ device_token: "lv_aud_device",
327
+ /** Session JWT for direct mode (1yr expiry, rotated on /welcome, analytics-only scope) */
328
+ session_token: "lv_aud_session",
329
+ /** Cached insights mode ('proxy' | 'direct') */
330
+ mode: "lv_aud_mode"
331
+ };
332
+ let _insightsMode = "proxy";
333
+ let _insightsBaseUrl = "";
334
+ let _localStorageAvailable = true;
335
+ const _memoryStorage = /* @__PURE__ */ new Map();
336
+ const checkLocalStorageAvailable = () => {
337
+ if (typeof window === "undefined" || !("localStorage" in window)) {
338
+ return false;
339
+ }
340
+ try {
341
+ const testKey = "__levo_test__";
342
+ window.localStorage.setItem(testKey, "test");
343
+ window.localStorage.removeItem(testKey);
344
+ return true;
345
+ } catch {
346
+ return false;
347
+ }
348
+ };
349
+ const getFromStorage = (key) => {
350
+ if (typeof window === "undefined") {
351
+ return _memoryStorage.get(key) || null;
352
+ }
353
+ if (_localStorageAvailable) {
354
+ try {
355
+ return window.localStorage.getItem(key);
356
+ } catch {
357
+ return _memoryStorage.get(key) || null;
358
+ }
359
+ }
360
+ return _memoryStorage.get(key) || null;
361
+ };
362
+ const setInStorage = (key, value) => {
363
+ if (typeof window === "undefined") {
364
+ _memoryStorage.set(key, value);
365
+ return;
366
+ }
367
+ if (_localStorageAvailable) {
368
+ try {
369
+ window.localStorage.setItem(key, value);
370
+ return;
371
+ } catch {
372
+ }
373
+ }
374
+ _memoryStorage.set(key, value);
375
+ };
376
+ const removeFromStorage = (key) => {
377
+ _memoryStorage.delete(key);
378
+ if (typeof window === "undefined" || !_localStorageAvailable) {
379
+ return;
380
+ }
381
+ try {
382
+ window.localStorage.removeItem(key);
383
+ } catch {
384
+ }
385
+ };
386
+ const getAuthHeaders = () => {
387
+ const headers = {};
388
+ if (_insightsMode === "direct") {
389
+ const deviceToken = getFromStorage(storage_keys.device_token);
390
+ const sessionToken = getFromStorage(storage_keys.session_token);
391
+ if (deviceToken) headers["Levo-Audience-Device"] = deviceToken;
392
+ if (sessionToken) headers["Levo-Audience-Session"] = sessionToken;
393
+ }
394
+ return headers;
395
+ };
396
+ const pingEndpoint = async (url, timeout) => {
397
+ let timeoutId = null;
398
+ try {
399
+ const controller = new AbortController();
400
+ timeoutId = setTimeout(() => controller.abort(), timeout);
401
+ const response = await fetch(url, {
402
+ method: "GET",
403
+ signal: controller.signal,
404
+ credentials: "omit"
405
+ });
406
+ return response.ok;
407
+ } catch {
408
+ return false;
409
+ } finally {
410
+ if (timeoutId) {
411
+ clearTimeout(timeoutId);
412
+ }
413
+ }
414
+ };
415
+ const detectInsightsMode = async () => {
416
+ if (insightsMode === "proxy") {
417
+ _insightsBaseUrl = insightsUrl;
418
+ return "proxy";
419
+ }
420
+ if (insightsMode === "direct") {
421
+ if (!directInsightsUrl) {
422
+ console.warn(
423
+ "[@levo-so/core] insightsMode='direct' but directInsightsUrl not set. Falling back to proxy."
424
+ );
425
+ _insightsBaseUrl = insightsUrl;
426
+ return "proxy";
427
+ }
428
+ _insightsBaseUrl = directInsightsUrl;
429
+ setInStorage(storage_keys.mode, "direct");
430
+ return "direct";
431
+ }
432
+ const cachedMode = getFromStorage(storage_keys.mode);
433
+ if (cachedMode === "proxy" || cachedMode === "direct") {
434
+ if (cachedMode === "direct" && !directInsightsUrl) {
435
+ removeFromStorage(storage_keys.mode);
436
+ _insightsBaseUrl = "";
437
+ } else {
438
+ const pingUrl = cachedMode === "proxy" ? `${insightsUrl}/ping` : `${directInsightsUrl}/ping`;
439
+ if (await pingEndpoint(pingUrl, pingTimeout)) {
440
+ _insightsBaseUrl = cachedMode === "proxy" ? insightsUrl : directInsightsUrl;
441
+ return cachedMode;
442
+ }
443
+ }
444
+ }
445
+ if (await pingEndpoint(`${insightsUrl}/ping`, pingTimeout)) {
446
+ setInStorage(storage_keys.mode, "proxy");
447
+ _insightsBaseUrl = insightsUrl;
448
+ return "proxy";
449
+ }
450
+ if (directInsightsUrl) {
451
+ setInStorage(storage_keys.mode, "direct");
452
+ _insightsBaseUrl = directInsightsUrl;
453
+ return "direct";
454
+ }
455
+ _insightsBaseUrl = insightsUrl;
456
+ return "proxy";
457
+ };
458
+ let _session = "";
459
+ let _device = "";
460
+ let is_identified = false;
461
+ let _eventCount = 0;
462
+ let _eventQueue = [];
463
+ let _flushTimer = null;
464
+ let _visibilityHandler = null;
465
+ let _beforeUnloadHandler = null;
466
+ let _pageHideHandler = null;
467
+ let _channel = null;
468
+ let _pendingIdentify = false;
469
+ const buildSessionSyncMessage = () => {
470
+ const message = {
471
+ type: "SESSION_SYNC",
472
+ session: _session,
473
+ device: _device
474
+ };
475
+ if (_insightsMode === "direct") {
476
+ const storedDeviceToken = getFromStorage(storage_keys.device_token);
477
+ const storedSessionToken = getFromStorage(storage_keys.session_token);
478
+ if (storedDeviceToken) message.deviceToken = storedDeviceToken;
479
+ if (storedSessionToken) message.sessionToken = storedSessionToken;
480
+ }
481
+ return message;
482
+ };
483
+ const initBroadcastChannel = () => {
484
+ if (typeof BroadcastChannel === "undefined") {
485
+ return;
486
+ }
487
+ try {
488
+ _channel = new BroadcastChannel(BROADCAST_CHANNEL_NAME);
489
+ _channel.onmessage = (event) => {
490
+ const { type, session, device, deviceToken, sessionToken } = event.data;
491
+ if (type === "SESSION_SYNC" && !is_identified) {
492
+ _session = session;
493
+ _device = device;
494
+ is_identified = true;
495
+ _pendingIdentify = false;
496
+ if (_insightsMode === "direct" && deviceToken && sessionToken) {
497
+ setInStorage(storage_keys.device_token, deviceToken);
498
+ setInStorage(storage_keys.session_token, sessionToken);
499
+ }
500
+ }
501
+ if (type === "SESSION_REQUEST" && is_identified) {
502
+ _channel?.postMessage(buildSessionSyncMessage());
503
+ }
504
+ };
505
+ } catch {
506
+ }
507
+ };
508
+ const requestSessionFromOtherTabs = () => {
509
+ _channel?.postMessage({ type: "SESSION_REQUEST" });
510
+ };
511
+ const broadcastSession = () => {
512
+ _channel?.postMessage(buildSessionSyncMessage());
513
+ };
514
+ const flushEventQueue = () => {
515
+ const baseUrl = _insightsBaseUrl || insightsUrl;
516
+ if (_eventQueue.length === 0 || !baseUrl || !workspace) {
517
+ return;
518
+ }
519
+ const batch = _eventQueue.map((event) => ({
520
+ ...event,
521
+ session_id: event.session_id || _session,
522
+ device_id: event.device_id || _device
523
+ }));
524
+ _eventQueue = [];
525
+ const url = `${baseUrl}/v1/insights/event/bulk?workspace=${workspace}`;
526
+ const headers = {
527
+ "Content-Type": "application/json",
528
+ ...getAuthHeaders()
529
+ };
530
+ if (_insightsMode === "proxy" && navigator.sendBeacon) {
531
+ const success = navigator.sendBeacon(url, JSON.stringify(batch));
532
+ if (success) {
533
+ return;
534
+ }
535
+ }
536
+ fetch(url, {
537
+ method: "POST",
538
+ body: JSON.stringify(batch),
539
+ headers,
540
+ keepalive: true,
541
+ credentials: _insightsMode === "proxy" ? "include" : "omit"
542
+ }).catch(() => {
543
+ });
544
+ };
545
+ const startFlushTimer = () => {
546
+ if (_flushTimer) {
547
+ return;
548
+ }
549
+ _flushTimer = setInterval(() => {
550
+ flushEventQueue();
551
+ }, BATCH_INTERVAL_MS);
552
+ if (typeof document !== "undefined") {
553
+ _visibilityHandler = () => {
554
+ if (document.visibilityState === "hidden") {
555
+ flushEventQueue();
556
+ }
557
+ };
558
+ _beforeUnloadHandler = () => {
559
+ flushEventQueue();
560
+ };
561
+ _pageHideHandler = () => {
562
+ flushEventQueue();
563
+ };
564
+ document.addEventListener("visibilitychange", _visibilityHandler);
565
+ window.addEventListener("beforeunload", _beforeUnloadHandler);
566
+ window.addEventListener("pagehide", _pageHideHandler);
567
+ }
568
+ };
569
+ const stopFlushTimer = () => {
570
+ if (_flushTimer) {
571
+ clearInterval(_flushTimer);
572
+ _flushTimer = null;
573
+ }
574
+ if (typeof document !== "undefined") {
575
+ if (_visibilityHandler) {
576
+ document.removeEventListener("visibilitychange", _visibilityHandler);
577
+ _visibilityHandler = null;
578
+ }
579
+ if (_beforeUnloadHandler) {
580
+ window.removeEventListener("beforeunload", _beforeUnloadHandler);
581
+ _beforeUnloadHandler = null;
582
+ }
583
+ if (_pageHideHandler) {
584
+ window.removeEventListener("pagehide", _pageHideHandler);
585
+ _pageHideHandler = null;
586
+ }
587
+ }
588
+ };
589
+ const queueEvent = (collectInput) => {
590
+ if (_eventCount >= MAX_EVENTS_PER_PAGE) {
591
+ console.warn(`[@levo-so/core] Event limit (${MAX_EVENTS_PER_PAGE}) reached for this page`);
592
+ return;
593
+ }
594
+ _eventCount++;
595
+ _eventQueue.push(collectInput);
596
+ if (_eventQueue.length >= BATCH_SIZE) {
597
+ flushEventQueue();
598
+ }
599
+ };
600
+ const getReferrer = () => {
601
+ if (typeof document === "undefined") {
602
+ return {};
603
+ }
604
+ const {
605
+ type,
606
+ referrer,
607
+ data = {}
608
+ } = getVisitorSource({
609
+ referrer: document.referrer,
610
+ currentUrl: window.location.href,
611
+ mapper: null
612
+ });
613
+ const dataKeys = Object.keys(data);
614
+ for (const key of dataKeys) {
615
+ if (data[key] && Array.isArray(data[key]) && data[key].length > 0) {
616
+ data[key] = data[key]?.[0];
617
+ }
618
+ }
619
+ return { type, referrer, data, raw: document.referrer };
620
+ };
621
+ const getCurrentTabId = () => {
622
+ try {
623
+ if (typeof window === "undefined" || !("sessionStorage" in window)) {
624
+ return "";
625
+ }
626
+ return window.sessionStorage.getItem(storage_keys.current_tab) || "";
627
+ } catch {
628
+ return "";
629
+ }
630
+ };
631
+ const getTabCount = () => {
632
+ try {
633
+ if (typeof window === "undefined" || !("localStorage" in window)) {
634
+ return 0;
635
+ }
636
+ return Number(window.localStorage.getItem(storage_keys.open_tabs)) || 0;
637
+ } catch {
638
+ return 0;
639
+ }
640
+ };
641
+ const getActivityStatus = () => {
642
+ const status = idle_listener?.getStatus();
643
+ if (!status) {
644
+ return null;
645
+ }
646
+ return {
647
+ status: status.isIdle ? "idle" : "active",
648
+ seconds: status.isIdle ? status.idle : status.active
649
+ };
650
+ };
651
+ const identify = async (options2) => {
652
+ if (!instance) {
653
+ return;
654
+ }
655
+ const withLock = options2?.withLock ?? true;
656
+ const referrer = getReferrer();
657
+ const traits = await getUserProperties();
658
+ await instance.identify("", {
659
+ ...traits,
660
+ referrer,
661
+ withLock
662
+ });
663
+ };
664
+ const request = async (url, data) => {
665
+ const headers = {
666
+ Accept: "application/json",
667
+ "Content-Type": "application/json",
668
+ ...getAuthHeaders()
669
+ };
670
+ try {
671
+ const response = await levoFetch.url(_insightsBaseUrl || insightsUrl, true).url(url).options({
672
+ keepAlive: true,
673
+ credentials: _insightsMode === "proxy" ? "include" : "omit"
674
+ }).headers(headers).post(data).json();
675
+ return response;
676
+ } catch (error) {
677
+ console.warn(`Analytics request failed for ${url}:`, error);
678
+ return null;
679
+ }
680
+ };
681
+ const initiate = async (properties) => {
682
+ if (NODE_ENV === "development") {
683
+ return;
684
+ }
685
+ _eventCount = 0;
686
+ _localStorageAvailable = checkLocalStorageAvailable();
687
+ _insightsMode = await detectInsightsMode();
688
+ initBroadcastChannel();
689
+ requestSessionFromOtherTabs();
690
+ startFlushTimer();
691
+ await new Promise((resolve) => setTimeout(resolve, 50));
692
+ const plugin = {
693
+ name: workspace,
694
+ /**
695
+ * Handle track events (custom events like clicks, scrolls, etc.)
696
+ *
697
+ * Enriches the event with:
698
+ * - Session/device IDs
699
+ * - Page metadata (URL, title, etc.)
700
+ * - Tab information
701
+ * - Timestamp
702
+ *
703
+ * Then queues for batched sending.
704
+ */
705
+ track: ({ payload }) => {
706
+ if (!insightsUrl || !workspace) {
707
+ return;
708
+ }
709
+ const payloadProperties = payload?.properties || {};
710
+ const collectInput = {
711
+ session_id: _session,
712
+ device_id: _device,
713
+ event: payload.event,
714
+ version: payload.version || 1,
715
+ workspace_id: workspace,
716
+ site_id: site || void 0,
717
+ properties: {
718
+ ...properties,
719
+ ...payloadProperties,
720
+ // Remove metadata fields from properties (they're top-level)
721
+ version: void 0,
722
+ resource: void 0,
723
+ identifier: void 0,
724
+ hostname: void 0,
725
+ pathname: void 0,
726
+ url: void 0,
727
+ page_title: void 0,
728
+ created_at: void 0
729
+ },
730
+ hostname: payload?.hostname || window.location.hostname,
731
+ pathname: payload?.pathname || window.location.pathname,
732
+ url: payload?.url || window.location.href,
733
+ page_title: payload?.page_title || document.title,
734
+ resource: payloadProperties?.resource || "page",
735
+ identifier: payloadProperties?.identifier || properties?.id,
736
+ created_at: payload?.created_at || (/* @__PURE__ */ new Date()).toISOString(),
737
+ tab_id: getCurrentTabId(),
738
+ tab_count: getTabCount()
739
+ };
740
+ queueEvent(collectInput);
741
+ },
742
+ /**
743
+ * Handle page view events.
744
+ *
745
+ * Called when navigating to a new page (in SPAs) or on initial load.
746
+ * Resets the rate limit counter and tracks a page.view event.
747
+ */
748
+ page: ({ payload }) => {
749
+ if (!insightsUrl) {
750
+ return;
751
+ }
752
+ _eventCount = 0;
753
+ const payloadProperties = payload?.properties || {};
754
+ track("page.view", {
755
+ ...properties,
756
+ ...payloadProperties,
757
+ title: document.title,
758
+ identifier: properties.id,
759
+ resource: "page"
760
+ });
761
+ },
762
+ /**
763
+ * Handle identify events (session establishment).
764
+ *
765
+ * This is called when identify() is invoked. It:
766
+ * 1. Checks if already identified (via BroadcastChannel) - if so, skip
767
+ * 2. Calls /welcome API to get session/device IDs
768
+ * 3. On success, broadcasts session to other tabs
769
+ *
770
+ * The /welcome API uses HttpOnly cookies to maintain persistent
771
+ * device identity across sessions.
772
+ */
773
+ identify: ({ payload }) => {
774
+ if (!insightsUrl || !workspace) {
775
+ return;
776
+ }
777
+ if (is_identified) {
778
+ return;
779
+ }
780
+ if (_pendingIdentify) {
781
+ return;
782
+ }
783
+ _pendingIdentify = true;
784
+ const welcomeInput = {
785
+ private_mode: payload.traits.private_mode,
786
+ dark_mode: payload.traits.dark_mode,
787
+ timezone: payload.traits.timezone,
788
+ locale: payload.traits.locale,
789
+ referrer: payload.traits.referrer,
790
+ properties: payload.traits.properties,
791
+ workspace_id: workspace,
792
+ site_id: site || void 0,
793
+ hostname: window.location.hostname,
794
+ pathname: window.location.pathname,
795
+ page_title: document.title,
796
+ url: window.location.href,
797
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
798
+ tab_id: getCurrentTabId(),
799
+ tab_count: getTabCount()
800
+ };
801
+ request(
802
+ `/v1/insights/event/welcome`,
803
+ welcomeInput
804
+ ).then((data) => {
805
+ if (is_identified) {
806
+ _pendingIdentify = false;
807
+ return;
808
+ }
809
+ is_identified = true;
810
+ _pendingIdentify = false;
811
+ if (data?.content?.data?.session) {
812
+ _session = data?.content.data.session;
813
+ }
814
+ if (data?.content?.data?.device) {
815
+ _device = data?.content.data.device;
816
+ }
817
+ if (_insightsMode === "direct" && data?.content?.meta) {
818
+ const meta = data.content.meta;
819
+ if (meta.device_token) {
820
+ setInStorage(storage_keys.device_token, meta.device_token);
821
+ }
822
+ if (meta.session_token) {
823
+ setInStorage(storage_keys.session_token, meta.session_token);
824
+ }
825
+ }
826
+ broadcastSession();
827
+ }).catch((e) => {
828
+ _pendingIdentify = false;
829
+ return getLevoError(e);
830
+ });
831
+ }
832
+ };
833
+ instance = Analytics({
834
+ app: workspace || "",
835
+ plugins: [plugin]
836
+ });
837
+ await identify();
838
+ startActivityListener();
839
+ return instance.page({
840
+ resource: "page",
841
+ identifier: properties.id
842
+ });
843
+ };
844
+ const startActivityListener = () => {
845
+ if (typeof document !== "undefined" && !idle_listener) {
846
+ idle_listener = onUserActivity({
847
+ timeout: _IDLE_TIMEOUT,
848
+ throttle: _IDLE_THROTTLE,
849
+ onIdle: (activeForSeconds) => {
850
+ track("user.idle", {
851
+ seconds_active: activeForSeconds
852
+ });
853
+ },
854
+ onWakeUp: (idleForSeconds) => {
855
+ track("user.active", {
856
+ seconds_idle: idleForSeconds
857
+ });
858
+ }
859
+ });
860
+ }
861
+ return null;
862
+ };
863
+ const bounce = async (properties) => {
864
+ const activity_status = getActivityStatus();
865
+ if (activity_status) {
866
+ if (activity_status?.status === "idle") {
867
+ properties.seconds_idle = activity_status.seconds;
868
+ } else {
869
+ properties.seconds_active = activity_status.seconds;
870
+ }
871
+ }
872
+ track("page.bounce", properties);
873
+ };
874
+ const track = async (event, properties) => {
875
+ if (!workspace) {
876
+ return;
877
+ }
878
+ if (instance) {
879
+ return instance.track(event, properties);
880
+ }
881
+ const collectInput = {
882
+ event,
883
+ version: properties?.version || 1,
884
+ properties: {
885
+ ...properties,
886
+ version: void 0,
887
+ resource: void 0,
888
+ identifier: void 0,
889
+ hostname: void 0,
890
+ pathname: void 0,
891
+ url: void 0,
892
+ page_title: void 0,
893
+ created_at: void 0
894
+ },
895
+ hostname: window.location.hostname,
896
+ pathname: window.location.pathname,
897
+ url: window.location.href,
898
+ page_title: document.title,
899
+ resource: properties?.resource || "page",
900
+ identifier: properties?.identifier || properties?.id,
901
+ workspace_id: workspace,
902
+ site_id: site || void 0,
903
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
904
+ tab_id: getCurrentTabId(),
905
+ tab_count: getTabCount()
906
+ };
907
+ queueEvent(collectInput);
908
+ };
909
+ const destroy = () => {
910
+ flushEventQueue();
911
+ stopFlushTimer();
912
+ _channel?.close();
913
+ };
914
+ return {
915
+ get insightsUrl() {
916
+ return insightsUrl;
917
+ },
918
+ /** Get the underlying analytics instance (read-only) */
919
+ get instance() {
920
+ return instance;
921
+ },
922
+ /** Check if session has been established (read-only) */
923
+ get is_identified() {
924
+ return is_identified;
925
+ },
926
+ /** Storage keys used by the module */
927
+ storage_keys,
928
+ /** Initialize the module for a page */
929
+ initiate,
930
+ /** Identify/establish session */
931
+ identify,
932
+ /** Track a custom event */
933
+ track,
934
+ /** Track a bounce event */
935
+ bounce,
936
+ /** Cleanup the module */
937
+ destroy
938
+ };
939
+ };
940
+
941
+ export { AnalyticsEvents, AnalyticsEventsList, createLevoInsights };
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@levo-so/insights",
3
+ "version": "0.1.61",
4
+ "author": "Levo Engineering <devs@theinternetfolks.com>",
5
+ "description": "Levo analytics",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "files": [
12
+ "dist/**",
13
+ "!dist/**/*.d.ts.map"
14
+ ],
15
+ "scripts": {
16
+ "clean": "rimraf dist node_modules .turbo",
17
+ "check-types": "tsc --noEmit",
18
+ "build": "tsup",
19
+ "dev": "tsup --watch",
20
+ "demo:serve": "python3 -m http.server 8080",
21
+ "demo": "pnpm build && pnpm demo:serve"
22
+ },
23
+ "dependencies": {
24
+ "@analytics/activity-utils": "0.2.2",
25
+ "@analytics/visitor-source": "0.0.7",
26
+ "@levo-so/core": "workspace:*",
27
+ "analytics": "0.8.14",
28
+ "get-user-locale": "2.3.2",
29
+ "lodash-es": "4.17.21"
30
+ },
31
+ "devDependencies": {
32
+ "@levo/ts-config": "workspace:*",
33
+ "@types/lodash-es": "4.17.12",
34
+ "tsup": "8.2.4",
35
+ "typescript": "5.9.3"
36
+ },
37
+ "main": "./dist/index.js",
38
+ "module": "./dist/index.js",
39
+ "types": "./dist/index.d.ts",
40
+ "exports": {
41
+ ".": {
42
+ "types": "./dist/index.d.ts",
43
+ "default": "./dist/index.js"
44
+ }
45
+ }
46
+ }