plugeen 0.0.7 → 0.0.9

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.cjs CHANGED
@@ -1,651 +1,528 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
-
20
- // src/index.ts
21
- var src_exports = {};
22
- __export(src_exports, {
23
- createPlugeen: () => createPlugeen
24
- });
25
- module.exports = __toCommonJS(src_exports);
1
+ 'use strict';
26
2
 
27
- // src/lib/helpers/brand.ts
28
- var APP_NAME = "Plugeen";
29
- var APP_PREFIX = APP_NAME.toLowerCase().slice(0, 2);
30
-
31
- // src/lib/helpers/storage.ts
32
- function withPrefix(key) {
33
- return `${APP_PREFIX}_${key}`;
34
- }
35
- function ensureServerStore() {
36
- if (!globalThis.Plugeen) {
37
- globalThis.Plugeen = {};
3
+ // src/helpers/uuid.ts
4
+ function generateUUID() {
5
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
6
+ return crypto.randomUUID();
38
7
  }
39
- return globalThis.Plugeen;
40
- }
41
- function serverStorage() {
42
- const set = (key, value) => {
43
- ensureServerStore()[key] = value;
44
- return value;
45
- };
46
- const get = (key) => ensureServerStore()[key];
47
- return {
48
- get,
49
- set
50
- };
8
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
9
+ const bytes = new Uint8Array(16);
10
+ crypto.getRandomValues(bytes);
11
+ bytes[6] = bytes[6] & 15 | 64;
12
+ bytes[8] = bytes[8] & 63 | 128;
13
+ const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
14
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
15
+ }
16
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
17
+ const r = Math.floor(Math.random() * 16);
18
+ return (c === "x" ? r : r & 3 | 8).toString(16);
19
+ });
51
20
  }
52
- function clientStorage() {
53
- const set = (key, value) => {
54
- localStorage.setItem(
55
- withPrefix(key),
56
- typeof value === "string" ? value : JSON.stringify(value)
57
- );
58
- return value;
59
- };
60
- const get = (key) => {
61
- try {
62
- const raw = localStorage.getItem(withPrefix(key));
63
- if (!raw) throw new Error("No data found");
64
- if (typeof raw === "string") return raw;
65
- return JSON.parse(raw);
66
- } catch {
67
- return void 0;
21
+
22
+ // src/storage.ts
23
+ var IDENTITY_KEY = "plugeen_anon_id";
24
+ var SESSION_KEY = "plugeen_session_id";
25
+ var SESSION_TS_KEY = "plugeen_session_ts";
26
+ var SESSION_TTL = 18e5;
27
+ var memStore = {};
28
+ function getOrCreateIdentityId() {
29
+ try {
30
+ let id = localStorage.getItem(IDENTITY_KEY);
31
+ if (!id) {
32
+ id = `anon_${generateUUID()}`;
33
+ localStorage.setItem(IDENTITY_KEY, id);
68
34
  }
69
- };
70
- return {
71
- get,
72
- set
73
- };
74
- }
75
- function getStorage() {
76
- if (typeof window === "undefined") return serverStorage();
77
- return clientStorage();
35
+ return id;
36
+ } catch {
37
+ if (!memStore[IDENTITY_KEY]) {
38
+ memStore[IDENTITY_KEY] = `anon_${generateUUID()}`;
39
+ }
40
+ return memStore[IDENTITY_KEY];
41
+ }
78
42
  }
79
-
80
- // src/lib/helpers/identity.ts
81
- function getOrCreateIdentity() {
82
- const storage = getStorage();
83
- const identity = storage.get("identity");
84
- if (identity) return identity;
85
- const newId = crypto.randomUUID();
86
- storage.set("identity", newId);
87
- return newId;
43
+ function setIdentityId(id) {
44
+ try {
45
+ localStorage.setItem(IDENTITY_KEY, id);
46
+ } catch {
47
+ memStore[IDENTITY_KEY] = id;
48
+ }
88
49
  }
89
-
90
- // src/lib/helpers/session.ts
91
- var SESSION_KEY = "pl_session";
92
50
  function getOrCreateSessionId() {
93
51
  try {
94
- if (typeof window === "undefined") return "";
95
52
  const existing = sessionStorage.getItem(SESSION_KEY);
96
- if (existing) return existing;
97
- throw new Error("No session ID");
98
- } catch {
99
- const id = crypto.randomUUID();
53
+ const ts = sessionStorage.getItem(SESSION_TS_KEY);
54
+ if (existing && ts && Date.now() - parseInt(ts, 10) < SESSION_TTL) {
55
+ sessionStorage.setItem(SESSION_TS_KEY, String(Date.now()));
56
+ return existing;
57
+ }
58
+ sessionStorage.removeItem(SESSION_KEY);
59
+ sessionStorage.removeItem(SESSION_TS_KEY);
60
+ const id = `sess_${generateUUID()}`;
100
61
  sessionStorage.setItem(SESSION_KEY, id);
62
+ sessionStorage.setItem(SESSION_TS_KEY, String(Date.now()));
63
+ return id;
64
+ } catch {
65
+ const existing = memStore[SESSION_KEY];
66
+ const ts = memStore[SESSION_TS_KEY];
67
+ if (existing && ts && Date.now() - parseInt(ts, 10) < SESSION_TTL) {
68
+ memStore[SESSION_TS_KEY] = String(Date.now());
69
+ return existing;
70
+ }
71
+ const id = `sess_${generateUUID()}`;
72
+ memStore[SESSION_KEY] = id;
73
+ memStore[SESSION_TS_KEY] = String(Date.now());
101
74
  return id;
102
75
  }
103
76
  }
104
77
 
105
- // src/lib/helpers/api/index.ts
106
- function getHeaders(apiKey, method) {
107
- const headers = new Headers();
108
- const identity = getStorage().get("identity");
109
- headers.append("x-identity-id", identity || getOrCreateIdentity());
110
- headers.set("Authorization", `Bearer ${apiKey}`);
111
- headers.set("x-session-id", getOrCreateSessionId());
112
- if (method === "POST" || method === "PUT") {
113
- headers.set("Content-Type", "application/json");
78
+ // src/helpers/http-client/index.ts
79
+ var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
80
+ var HttpClient = class {
81
+ constructor(baseUrl, apiKey, maxRetries = 3) {
82
+ this.baseUrl = baseUrl;
83
+ this.apiKey = apiKey;
84
+ this.maxRetries = maxRetries;
114
85
  }
115
- return headers;
116
- }
117
- async function fetcher(params) {
118
- return fetch(`${params.options.baseUrl}${params.url}`, {
119
- method: params.method,
120
- body: params.body ? JSON.stringify({ ...params.body, source: "api" }) : void 0,
121
- headers: getHeaders(params.apiKey, params.method)
122
- }).then((response) => response.json()).then((response) => response.data);
123
- }
124
- var createApi = (apiKey, options) => {
86
+ headers(method) {
87
+ const h = {
88
+ Authorization: `Bearer ${this.apiKey}`,
89
+ "x-identity-id": getOrCreateIdentityId(),
90
+ "x-session-id": getOrCreateSessionId()
91
+ };
92
+ if (method !== "GET") h["Content-Type"] = "application/json";
93
+ return h;
94
+ }
95
+ async request(path, { attempt = 0, method, body }) {
96
+ const url = `${this.baseUrl}${path}`;
97
+ try {
98
+ const res = await fetch(url, {
99
+ method,
100
+ headers: this.headers(method),
101
+ credentials: "omit",
102
+ body: body ? JSON.stringify(body) : void 0
103
+ });
104
+ if (res.status === 401 || res.status === 404) return [null, res.status];
105
+ if ((res.status >= 500 || res.status === 429) && attempt < this.maxRetries) {
106
+ const jitter = 0.85 + Math.random() * 0.3;
107
+ await delay(500 * 2 ** attempt * jitter);
108
+ return this.request(path, {
109
+ body,
110
+ attempt: attempt + 1,
111
+ method
112
+ });
113
+ }
114
+ if (res.status >= 200 && res.status < 300) {
115
+ try {
116
+ const json = await res.json();
117
+ return [json.data, null];
118
+ } catch {
119
+ return [null, "Failed to parse json"];
120
+ }
121
+ }
122
+ return [null, "Error"];
123
+ } catch (err) {
124
+ if (err instanceof TypeError && attempt < this.maxRetries) {
125
+ const jitter = 0.85 + Math.random() * 0.3;
126
+ await delay(500 * 2 ** attempt * jitter);
127
+ return this.get(path, attempt + 1);
128
+ }
129
+ return [null, err];
130
+ }
131
+ }
132
+ async post(path, body, attempt = 0) {
133
+ return this.request(path, {
134
+ attempt,
135
+ body,
136
+ method: "POST"
137
+ });
138
+ }
139
+ async get(path, attempt = 0) {
140
+ return this.request(path, {
141
+ attempt,
142
+ method: "GET"
143
+ });
144
+ }
145
+ beacon(path, body) {
146
+ if (typeof navigator === "undefined" || !navigator.sendBeacon) return false;
147
+ try {
148
+ const blob = new Blob([JSON.stringify(body)], {
149
+ type: "application/json"
150
+ });
151
+ return navigator.sendBeacon(`${this.baseUrl}${path}`, blob);
152
+ } catch {
153
+ return false;
154
+ }
155
+ }
156
+ };
157
+
158
+ // src/plugins/events/index.ts
159
+ function createEventsModule(http) {
125
160
  return {
126
- post: (url, body) => fetcher({ url, method: "POST", body, options, apiKey }),
127
- put: (url, body) => fetcher({ url, method: "PUT", body, options, apiKey }),
128
- get: (url) => fetcher({ url, method: "GET", options, apiKey })
161
+ create: async (name, data = {}) => {
162
+ return http.post("/events", { name, data, source: "api" });
163
+ }
129
164
  };
130
- };
165
+ }
131
166
 
132
- // src/lib/plugins/analytics/index.ts
133
- function initAnalytics(api) {
134
- const init = () => {
135
- const changePage = () => {
136
- api.post("/plugins/analytics", {
137
- event: "page_view",
138
- url: location.href,
139
- title: document.title,
140
- referrer: document.referrer ?? "",
141
- screenWidth: window.screen.width,
142
- screenHeight: window.screen.height,
143
- sessionId: getOrCreateSessionId()
167
+ // src/plugins/identities/index.ts
168
+ function createIdentitiesModule(http) {
169
+ return {
170
+ create: async (distinctId, data = {}) => {
171
+ const result = await http.post("/identities", {
172
+ id: distinctId,
173
+ ...data
144
174
  });
175
+ setIdentityId(distinctId);
176
+ return result;
177
+ }
178
+ };
179
+ }
180
+
181
+ // src/plugins/analytics/index.ts
182
+ function initAnalyticsPlugin(http) {
183
+ if (typeof window === "undefined") return;
184
+ let pageStartTime = Date.now();
185
+ let pageCount = 0;
186
+ let currentUrl = window.location.href;
187
+ const trackPageView = () => {
188
+ pageCount++;
189
+ void http.post("/v1/analytics", {
190
+ event: "page_view",
191
+ url: window.location.href,
192
+ title: document.title,
193
+ sessionId: getOrCreateSessionId(),
194
+ referrer: document.referrer || void 0,
195
+ screenWidth: window.innerWidth,
196
+ screenHeight: window.innerHeight
197
+ });
198
+ };
199
+ const trackPageExit = (useBeacon = false) => {
200
+ const data = {
201
+ event: "page_exit",
202
+ url: window.location.href,
203
+ title: document.title,
204
+ sessionId: getOrCreateSessionId(),
205
+ referrer: document.referrer || void 0,
206
+ screenWidth: window.innerWidth,
207
+ screenHeight: window.innerHeight,
208
+ metadata: {
209
+ timeOnPage: Math.round((Date.now() - pageStartTime) / 1e3),
210
+ pageCount
211
+ }
145
212
  };
146
- const pushState = history.pushState;
147
- history.pushState = function(...args) {
148
- pushState.apply(this, args);
149
- changePage();
150
- };
151
- const replaceState = history.replaceState;
152
- history.replaceState = function(...args) {
153
- replaceState.apply(this, args);
154
- changePage;
213
+ if (useBeacon) {
214
+ http.beacon("/v1/analytics", data);
215
+ } else {
216
+ void http.post("/v1/analytics", data);
217
+ }
218
+ };
219
+ const onRouteChange = () => {
220
+ if (window.location.href === currentUrl) return;
221
+ trackPageExit();
222
+ pageStartTime = Date.now();
223
+ currentUrl = window.location.href;
224
+ trackPageView();
225
+ };
226
+ const patchHistoryMethod = (method) => {
227
+ const original = history[method].bind(history);
228
+ history[method] = (...args) => {
229
+ original(...args);
230
+ onRouteChange();
155
231
  };
156
- window.addEventListener("popstate", () => changePage());
157
- changePage();
158
232
  };
159
- return init();
233
+ patchHistoryMethod("pushState");
234
+ patchHistoryMethod("replaceState");
235
+ window.addEventListener("popstate", onRouteChange);
236
+ window.addEventListener("beforeunload", () => trackPageExit(true));
237
+ document.addEventListener("visibilitychange", () => {
238
+ if (document.visibilityState === "hidden") {
239
+ trackPageExit(true);
240
+ }
241
+ });
242
+ trackPageView();
160
243
  }
161
244
 
162
- // src/lib/plugins/chat/index.tsx
163
- var import_signals2 = require("@preact/signals");
164
- var import_lucide_preact = require("lucide-preact");
165
-
166
- // src/store/theme.ts
167
- var import_signals = require("@preact/signals");
168
- var theme = (0, import_signals.signal)({
169
- accentColor: "",
170
- foregroundColor: ""
171
- });
172
-
173
- // src/components/button.tsx
174
- var import_jsx_runtime = require("preact/jsx-runtime");
175
- function Button({ children, disabled, fullWidth }) {
176
- const deactivated = disabled;
177
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
178
- "button",
179
- {
180
- type: "submit",
181
- disabled: deactivated,
182
- style: {
183
- background: theme.value.accentColor,
184
- color: theme.value.foregroundColor,
185
- width: fullWidth ? "100%" : "max-content",
186
- border: "none",
187
- borderRadius: "8px",
188
- padding: "8px 0",
189
- fontWeight: "600",
190
- fontSize: "13px",
191
- opacity: deactivated ? 0.5 : 1,
192
- cursor: deactivated ? "not allowed" : "pointer",
193
- transition: "all 0.4s ease"
194
- },
195
- children
245
+ // src/plugins/logs/index.ts
246
+ var EXTENSION_PREFIXES = [
247
+ "chrome-extension://",
248
+ "moz-extension://",
249
+ "safari-extension://",
250
+ "edge-extension://"
251
+ ];
252
+ function isExtensionSource(str) {
253
+ if (!str) return false;
254
+ const lower = str.toLowerCase();
255
+ return EXTENSION_PREFIXES.some((prefix) => lower.includes(prefix));
256
+ }
257
+ function initErrorsPlugin(http) {
258
+ if (typeof window === "undefined") return;
259
+ window.addEventListener("error", (event) => {
260
+ if (isExtensionSource(event.filename) || isExtensionSource(event.error?.stack))
261
+ return;
262
+ if (event.error === null && event.message === "Script error.") return;
263
+ void http.post("/v1/logs", {
264
+ message: event.message || "Unknown Error",
265
+ filename: event.filename,
266
+ lineno: event.lineno,
267
+ colno: event.colno,
268
+ stack: event.error?.stack,
269
+ error_type: event.error?.name || "Error"
270
+ });
271
+ });
272
+ window.addEventListener(
273
+ "unhandledrejection",
274
+ (event) => {
275
+ const { reason } = event;
276
+ if (isExtensionSource(reason?.stack))
277
+ return;
278
+ let message = "Unknown Error";
279
+ let stack;
280
+ if (reason instanceof Error) {
281
+ message = reason.message;
282
+ stack = reason.stack;
283
+ } else if (typeof reason === "string") {
284
+ message = reason;
285
+ } else if (reason !== null && typeof reason === "object" && "message" in reason) {
286
+ message = String(reason.message);
287
+ }
288
+ void http.post("/v1/logs", {
289
+ message,
290
+ stack,
291
+ error_type: "UnhandledRejection"
292
+ });
196
293
  }
197
294
  );
198
295
  }
199
296
 
200
- // src/hooks/use-query.ts
201
- var import_hooks = require("preact/hooks");
202
- var cache = /* @__PURE__ */ new Map();
203
- function serializeKey(key) {
204
- return typeof key === "string" ? key : JSON.stringify(key);
297
+ // src/plugins/web-vitals/helpers.ts
298
+ function getRating(value, thresholds) {
299
+ if (value > thresholds[1]) return "poor";
300
+ if (value > thresholds[0]) return "needs-improvement";
301
+ return "good";
302
+ }
303
+ function getActivationStart() {
304
+ const nav = performance.getEntriesByType(
305
+ "navigation"
306
+ )[0];
307
+ return nav?.activationStart ?? 0;
308
+ }
309
+ function getNavEntry() {
310
+ const entry = performance.getEntriesByType("navigation")[0];
311
+ if (entry && entry.responseStart > 0 && entry.responseStart < performance.now())
312
+ return entry;
313
+ }
314
+ function observe(type, cb, opts) {
315
+ try {
316
+ if (!PerformanceObserver.supportedEntryTypes.includes(type)) return;
317
+ const po = new PerformanceObserver((list) => {
318
+ Promise.resolve().then(() => cb(list.getEntries()));
319
+ });
320
+ po.observe({
321
+ type,
322
+ buffered: true,
323
+ ...opts ?? {}
324
+ });
325
+ return po;
326
+ } catch {
327
+ return void 0;
328
+ }
205
329
  }
206
- function useQuery({
207
- queryKey,
208
- queryFn,
209
- revalidateTime = 0
210
- }) {
211
- const key = serializeKey(queryKey);
212
- const [state, setState] = (0, import_hooks.useState)({
213
- data: void 0,
214
- error: void 0,
215
- isLoading: true,
216
- isFetching: false
330
+ function observeFCP(cb) {
331
+ const reported = /* @__PURE__ */ new Set();
332
+ observe("paint", (entries) => {
333
+ for (const entry of entries) {
334
+ if (entry.name === "first-contentful-paint" && !reported.has("FCP")) {
335
+ reported.add("FCP");
336
+ const value = Math.max(entry.startTime - getActivationStart(), 0);
337
+ cb({
338
+ name: "FCP",
339
+ value: Math.round(value),
340
+ rating: getRating(value, [1800, 3e3])
341
+ });
342
+ }
343
+ }
217
344
  });
218
- const mounted = (0, import_hooks.useRef)(true);
219
- async function fetchData(initial = false) {
220
- try {
221
- setState((prev) => ({
222
- ...prev,
223
- isLoading: initial,
224
- isFetching: !initial,
225
- error: void 0
226
- }));
227
- const data = await queryFn();
228
- cache.set(key, {
229
- data,
230
- updatedAt: Date.now()
231
- });
232
- if (!mounted.current) return;
233
- setState({
234
- data,
235
- error: void 0,
236
- isLoading: false,
237
- isFetching: false
238
- });
239
- } catch (error) {
240
- if (!mounted.current) return;
241
- setState({
242
- data: void 0,
243
- error,
244
- isLoading: false,
245
- isFetching: false
345
+ }
346
+ function observeLCP(cb) {
347
+ let reported = false;
348
+ const po = observe("largest-contentful-paint", (entries) => {
349
+ if (reported) return;
350
+ const last = entries[entries.length - 1];
351
+ if (last) {
352
+ const value = Math.max(last.startTime - getActivationStart(), 0);
353
+ cb({
354
+ name: "LCP",
355
+ value: Math.round(value),
356
+ rating: getRating(value, [2500, 4e3])
246
357
  });
247
358
  }
248
- }
249
- (0, import_hooks.useEffect)(() => {
250
- mounted.current = true;
251
- const cached = cache.get(key);
252
- if (cached) {
253
- const isStale = Date.now() - cached.updatedAt > revalidateTime;
254
- setState({
255
- data: cached.data,
256
- error: void 0,
257
- isLoading: false,
258
- isFetching: isStale
359
+ });
360
+ if (!po) return;
361
+ const finalize = () => {
362
+ if (reported) return;
363
+ reported = true;
364
+ const records = po.takeRecords();
365
+ if (records.length > 0) {
366
+ const last = records[records.length - 1];
367
+ const value = Math.max(last.startTime - getActivationStart(), 0);
368
+ cb({
369
+ name: "LCP",
370
+ value: Math.round(value),
371
+ rating: getRating(value, [2500, 4e3])
259
372
  });
260
- if (isStale) {
261
- fetchData(false);
262
- }
263
- } else {
264
- fetchData(true);
265
- }
266
- return () => {
267
- mounted.current = false;
268
- };
269
- }, [key]);
270
- (0, import_hooks.useEffect)(() => {
271
- let interval;
272
- if (revalidateTime > 0) {
273
- interval = setInterval(() => {
274
- fetchData(false);
275
- }, revalidateTime);
276
373
  }
277
- return () => {
278
- clearInterval(interval);
279
- };
280
- }, [revalidateTime]);
281
- return {
282
- ...state,
283
- refetch: () => fetchData(false)
374
+ po.disconnect();
284
375
  };
285
- }
286
-
287
- // src/lib/helpers/ui.ts
288
- var import_preact = require("preact");
289
- var noopUnmount = () => {
290
- };
291
- function getOrCreateRoot(target, id) {
292
- const existing = document.getElementById(id);
293
- if (existing instanceof HTMLDivElement) {
294
- return existing;
376
+ for (const evt of ["keydown", "click", "visibilitychange"]) {
377
+ addEventListener(evt, finalize, { capture: true, once: true });
295
378
  }
296
- const container = document.createElement("div");
297
- container.id = id;
298
- target.appendChild(container);
299
- return container;
300
379
  }
301
- function renderUI({
302
- component: Component,
303
- id,
304
- options
305
- }) {
306
- if (typeof document === "undefined") {
307
- return { render: () => void 0, unmount: noopUnmount };
308
- }
309
- theme.value = {
310
- accentColor: options.accentColor,
311
- foregroundColor: options.foregroundColor
312
- };
313
- const target = document.body;
314
- const mount = () => {
315
- const container = getOrCreateRoot(document.body, id);
316
- if (container.parentElement !== target) {
317
- target.appendChild(container);
318
- }
319
- (0, import_preact.render)(Component, container);
320
- return container;
321
- };
322
- return {
323
- render: mount,
324
- unmount: () => {
325
- const el = document.getElementById(id);
326
- if (el) {
327
- (0, import_preact.render)(null, el);
328
- el.remove();
380
+ function observeCLS(cb) {
381
+ let sessionValue = 0;
382
+ let sessionEntries = [];
383
+ let clsValue = 0;
384
+ observe("layout-shift", (entries) => {
385
+ for (const raw of entries) {
386
+ const entry = raw;
387
+ if (entry.hadRecentInput) continue;
388
+ const last = sessionEntries[sessionEntries.length - 1];
389
+ const first = sessionEntries[0];
390
+ if (sessionEntries.length > 0 && last && first && entry.startTime - last.startTime < 1e3 && entry.startTime - first.startTime < 5e3) {
391
+ sessionValue += entry.value;
392
+ sessionEntries.push(entry);
393
+ } else {
394
+ sessionValue = entry.value;
395
+ sessionEntries = [entry];
396
+ }
397
+ if (sessionValue > clsValue) {
398
+ clsValue = sessionValue;
399
+ const rounded = Math.round(clsValue * 1e4) / 1e4;
400
+ cb({
401
+ name: "CLS",
402
+ value: rounded,
403
+ rating: getRating(clsValue, [0.1, 0.25])
404
+ });
329
405
  }
330
406
  }
331
- };
332
- }
333
-
334
- // src/lib/plugins/chat/index.tsx
335
- var import_jsx_runtime2 = require("preact/jsx-runtime");
336
- var pluginName = "chats";
337
- var text = (0, import_signals2.signal)("");
338
- var open = (0, import_signals2.signal)(false);
339
- var submitting = (0, import_signals2.signal)(false);
340
- function FloatingChat({
341
- api,
342
- options: { accentColor, foregroundColor },
343
- id
344
- }) {
345
- const identity = getStorage().get("identity");
346
- const { data, refetch, isLoading, error } = useQuery({
347
- queryKey: [id],
348
- queryFn: async () => {
349
- return await api.get("/plugins/chats");
350
- },
351
- revalidateTime: 1e4
352
407
  });
353
- const onSubmit = async () => {
354
- submitting.value = true;
355
- await api.post("/plugins/chats", {
356
- text: text.value
357
- });
358
- await refetch();
359
- text.value = "";
360
- submitting.value = false;
361
- };
362
- if (!data && !error && isLoading) return null;
363
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
364
- "div",
365
- {
366
- style: {
367
- position: "fixed",
368
- right: "24px",
369
- bottom: "24px",
370
- zIndex: "9999",
371
- fontFamily: "system-ui, sans-serif"
372
- },
373
- children: [
374
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
375
- "div",
376
- {
377
- style: {
378
- width: "320px",
379
- marginBottom: "12px",
380
- borderRadius: "12px",
381
- boxShadow: "0 8px 32px rgba(0,0,0,0.18)",
382
- background: "#fff",
383
- overflow: "hidden",
384
- maxHeight: open.value ? "500px" : "0px",
385
- opacity: open.value ? 1 : 0,
386
- transition: "all 0.4s ease"
387
- },
388
- children: [
389
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
390
- "div",
391
- {
392
- style: {
393
- background: accentColor,
394
- color: "#fff",
395
- padding: "14px 16px",
396
- fontWeight: "600",
397
- fontSize: "14px"
398
- },
399
- children: "Chat with us"
400
- }
401
- ),
402
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
403
- "div",
404
- {
405
- style: {
406
- display: "grid",
407
- height: "200px",
408
- overflowY: "auto",
409
- padding: "16px",
410
- gap: "4px"
411
- },
412
- children: data?.messages.map((m, index) => {
413
- const isMine = identity === m.identity.id;
414
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
415
- "div",
416
- {
417
- style: {
418
- justifySelf: isMine ? "start" : "end",
419
- width: "max-content"
420
- },
421
- children: [
422
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
423
- "p",
424
- {
425
- style: {
426
- padding: "2px 8px",
427
- borderRadius: "8px",
428
- width: "max-content",
429
- background: isMine ? "silver" : accentColor,
430
- color: isMine ? "black" : foregroundColor
431
- },
432
- children: m.text
433
- }
434
- ),
435
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
436
- "span",
437
- {
438
- style: {
439
- fontSize: "12px",
440
- color: "silver",
441
- textAlign: isMine ? "left" : "right"
442
- },
443
- children: new Date(m.createdAt).toLocaleDateString()
444
- }
445
- )
446
- ]
447
- },
448
- m.text + index.toString()
449
- );
450
- })
451
- }
452
- ),
453
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
454
- "form",
455
- {
456
- onSubmit: (e) => {
457
- e.preventDefault();
458
- onSubmit();
459
- },
460
- style: { padding: "12px 16px" },
461
- children: [
462
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
463
- "textarea",
464
- {
465
- value: text.value,
466
- onInput: (e) => {
467
- text.value = e.target?.value;
468
- },
469
- name: "message",
470
- placeholder: "Send us a message\u2026",
471
- rows: 3,
472
- style: {
473
- width: "100%",
474
- border: "1px solid #e5e7eb",
475
- borderRadius: "8px",
476
- padding: "8px",
477
- fontSize: "13px",
478
- resize: "none",
479
- outline: "none",
480
- boxSizing: "border-box"
481
- }
482
- }
483
- ),
484
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Button, { fullWidth: true, disabled: !text.value, children: submitting.value ? "Sending" : "Send" })
485
- ]
486
- }
487
- )
488
- ]
489
- }
490
- ),
491
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
492
- "button",
493
- {
494
- type: "button",
495
- onClick: () => {
496
- open.value = !open.value;
497
- },
498
- "aria-label": "Open chat",
499
- style: {
500
- transform: `rotate(${open.value ? 180 : 0}deg)`,
501
- transition: "transform 0.4s ease",
502
- width: "52px",
503
- height: "52px",
504
- borderRadius: "50%",
505
- background: accentColor,
506
- border: "none",
507
- cursor: "pointer",
508
- display: "flex",
509
- alignItems: "center",
510
- justifyContent: "center",
511
- boxShadow: "0 4px 16px rgba(0,0,0,0.18)",
512
- marginLeft: "auto"
513
- },
514
- children: open.value ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_preact.X, { color: foregroundColor }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_preact.MessageCircle, { color: foregroundColor })
515
- }
516
- )
517
- ]
518
- }
519
- );
520
408
  }
521
- function initChat(api, options) {
522
- const id = crypto.randomUUID();
523
- const { render } = renderUI({
524
- component: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(FloatingChat, { id, api, options }),
525
- id: pluginName,
526
- options
409
+ function observeTTFB(cb) {
410
+ const nav = getNavEntry();
411
+ if (!nav) return;
412
+ const value = Math.max(nav.responseStart - getActivationStart(), 0);
413
+ cb({
414
+ name: "TTFB",
415
+ value: Math.round(value),
416
+ rating: getRating(value, [800, 1800])
527
417
  });
528
- return render();
529
- }
530
-
531
- // src/lib/plugins/events/index.ts
532
- function initEvents(api) {
533
- return {
534
- create: (eventName, data) => api.post("/events", {
535
- name: eventName,
536
- data
537
- })
538
- };
539
418
  }
540
-
541
- // src/lib/plugins/experiments/index.ts
542
- function initExperiments(api) {
543
- return {
544
- get: (experimentId) => api.get(`/plugins/experiments/${experimentId}`)
545
- };
419
+ function observeINP(cb) {
420
+ const interactions = /* @__PURE__ */ new Map();
421
+ let worstDuration = 0;
422
+ observe(
423
+ "event",
424
+ (entries) => {
425
+ for (const raw of entries) {
426
+ const entry = raw;
427
+ if (!entry.interactionId) continue;
428
+ const existing = interactions.get(entry.interactionId) ?? 0;
429
+ if (entry.duration > existing) {
430
+ interactions.set(entry.interactionId, entry.duration);
431
+ if (entry.duration > worstDuration) {
432
+ worstDuration = entry.duration;
433
+ cb({
434
+ name: "INP",
435
+ value: Math.round(entry.duration),
436
+ rating: getRating(entry.duration, [200, 500])
437
+ });
438
+ }
439
+ }
440
+ }
441
+ },
442
+ { durationThreshold: 40 }
443
+ );
546
444
  }
547
-
548
- // src/lib/plugins/feature-flags/index.ts
549
- function initFeatureFlags(api) {
550
- return {
551
- get: async (flagKey) => {
552
- return api.get(
553
- `/plugins/feature-flags/${flagKey}`
554
- );
445
+ function observeFPS(cb) {
446
+ if (typeof requestAnimationFrame === "undefined") return;
447
+ let frames = 0;
448
+ const duration = 2e3;
449
+ const start = performance.now();
450
+ const tick = () => {
451
+ frames++;
452
+ if (performance.now() - start < duration) {
453
+ requestAnimationFrame(tick);
454
+ } else {
455
+ cb({ name: "FPS", value: Math.round(frames / duration * 1e3) });
555
456
  }
556
457
  };
557
- }
558
-
559
- // src/lib/plugins/identities/index.ts
560
- function initIdentities(api) {
561
- const storage = getStorage();
562
- async function set(distinctId, data) {
563
- const res = await api.post("/identities", { ...data, id: distinctId });
564
- storage.set("identity", distinctId);
565
- return res;
458
+ if (document.readyState === "complete") {
459
+ requestAnimationFrame(tick);
460
+ } else {
461
+ window.addEventListener("load", () => requestAnimationFrame(tick), {
462
+ once: true
463
+ });
566
464
  }
567
- return {
568
- // get,
569
- set
570
- };
571
465
  }
572
-
573
- // src/lib/plugins/log-tracing/index.ts
574
- function initLogTracing(api) {
575
- return {
576
- send: (data) => api.post("/plugins/logs", data)
577
- };
466
+ function initWebVitals(cb) {
467
+ if (typeof window === "undefined" || typeof PerformanceObserver === "undefined")
468
+ return;
469
+ observeFCP(cb);
470
+ observeLCP(cb);
471
+ observeCLS(cb);
472
+ observeTTFB(cb);
473
+ observeINP(cb);
474
+ observeFPS(cb);
578
475
  }
579
476
 
580
- // src/lib/plugins/surveys/index.ts
581
- function initSurveys(api) {
582
- return {
583
- submit: (data) => api.post("/plugins/surveys", data)
584
- };
477
+ // src/plugins/web-vitals/index.ts
478
+ function initWebVitalsPlugin(http) {
479
+ initWebVitals((metric) => {
480
+ http.post("/v1/web-vitals", {
481
+ name: metric.name,
482
+ value: metric.value,
483
+ ...metric.rating !== void 0 ? { rating: metric.rating } : {}
484
+ });
485
+ });
585
486
  }
586
487
 
587
- // src/lib/plugins/index.ts
588
- var reset = `
589
- *, *::before, *::after { box-sizing: border-box; margin: 0; }
590
- body { line-height: 1.5; -webkit-font-smoothing: antialiased; }
591
- `;
592
- var initBaseSdk = (apiKey, options) => {
593
- const api = createApi(apiKey, options);
594
- return {
595
- events: initEvents(api),
596
- identities: initIdentities(api),
597
- featureFlags: initFeatureFlags(api),
598
- logs: initLogTracing(api),
599
- surveys: initSurveys(api),
600
- experiments: initExperiments(api)
488
+ // src/plugins/index.ts
489
+ function initPlugins(http, plugins) {
490
+ const initializers = {
491
+ analytics: initAnalyticsPlugin(http),
492
+ "web-vitals": initWebVitalsPlugin(http),
493
+ errors: initErrorsPlugin(http)
601
494
  };
602
- };
603
- function initClientSdk(apiKey, options) {
604
- const isBrowser = typeof window !== "undefined";
605
- const api = createApi(apiKey, options);
606
- if (isBrowser) {
607
- const style = document.createElement("style");
608
- style.innerHTML = reset;
609
- document.head.appendChild(style);
610
- if (options.plugins.includes("chats")) {
611
- initChat(api, options);
612
- }
613
- if (options.plugins.includes("analytics")) {
614
- initAnalytics(api);
615
- }
616
- return {};
617
- }
618
- return null;
495
+ plugins.map((p) => initializers[p]);
619
496
  }
620
497
 
621
- // src/lib/index.ts
622
- var defaultOptions = {
623
- baseUrl: "https://plugeen.app/api",
624
- accentColor: "#4f46e5",
625
- foregroundColor: "#fff",
498
+ // src/index.ts
499
+ var defaults = {
500
+ baseUrl: "https://dev.plugeen.app/api",
501
+ debug: false,
626
502
  plugins: []
627
503
  };
628
- var baseInstance = null;
629
- var clientInstance = null;
630
504
  function createPlugeen(apiKey, options) {
631
- const _options = {
632
- baseUrl: options?.baseUrl || defaultOptions.baseUrl,
633
- accentColor: options?.accentColor || defaultOptions.accentColor,
634
- foregroundColor: options?.foregroundColor || defaultOptions.foregroundColor,
635
- plugins: options?.plugins || []
505
+ const resolved = {
506
+ baseUrl: options.baseUrl || defaults.baseUrl,
507
+ debug: options.debug || defaults.debug,
508
+ plugins: options.plugins || defaults.plugins
636
509
  };
637
510
  if (!apiKey) {
638
- console.warn("[Plugeen] Missing data-api-key attribute.");
639
- }
640
- if (!baseInstance) {
641
- baseInstance = initBaseSdk(apiKey, _options);
511
+ throw new TypeError("[plugeen] apiKey is required");
642
512
  }
643
- if (!clientInstance) {
644
- clientInstance = initClientSdk(apiKey, _options);
513
+ const http = new HttpClient(resolved.baseUrl, apiKey);
514
+ const events = createEventsModule(http);
515
+ const identities = createIdentitiesModule(http);
516
+ const plugeen = {
517
+ track: (event, properties) => {
518
+ void events.create(event, properties);
519
+ },
520
+ identify: (userId, data) => identities.create(userId, data)
521
+ };
522
+ if (resolved.plugins.length > 0) {
523
+ initPlugins(http, resolved.plugins);
645
524
  }
646
- return baseInstance;
525
+ return plugeen;
647
526
  }
648
- // Annotate the CommonJS export names for ESM import in node:
649
- 0 && (module.exports = {
650
- createPlugeen
651
- });
527
+
528
+ exports.createPlugeen = createPlugeen;