plugeen 0.0.6 → 0.0.8

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,655 +1,656 @@
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);
26
-
27
- // src/lib/helpers/brand.ts
28
- var APP_NAME = "Plugeen";
29
- var APP_PREFIX = APP_NAME.toLowerCase().slice(0, 2);
1
+ 'use strict';
30
2
 
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
+ function getOrCreateIdentityId() {
28
+ try {
29
+ let id = localStorage.getItem(IDENTITY_KEY);
30
+ if (!id) {
31
+ id = `anon_${generateUUID()}`;
32
+ localStorage.setItem(IDENTITY_KEY, id);
68
33
  }
69
- };
70
- return {
71
- get,
72
- set
73
- };
74
- }
75
- function getStorage() {
76
- if (typeof window === "undefined") return serverStorage();
77
- return clientStorage();
34
+ return id;
35
+ } catch {
36
+ return `anon_${generateUUID()}`;
37
+ }
78
38
  }
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;
39
+ function setIdentityId(id) {
40
+ try {
41
+ localStorage.setItem(IDENTITY_KEY, id);
42
+ } catch {
43
+ }
88
44
  }
89
-
90
- // src/lib/helpers/session.ts
91
- var SESSION_KEY = "pl_session";
92
45
  function getOrCreateSessionId() {
93
46
  try {
94
- if (typeof window === "undefined") return "";
95
47
  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();
48
+ const ts = sessionStorage.getItem(SESSION_TS_KEY);
49
+ if (existing && ts && Date.now() - parseInt(ts, 10) < SESSION_TTL) {
50
+ sessionStorage.setItem(SESSION_TS_KEY, String(Date.now()));
51
+ return existing;
52
+ }
53
+ sessionStorage.removeItem(SESSION_KEY);
54
+ sessionStorage.removeItem(SESSION_TS_KEY);
55
+ const id = `sess_${generateUUID()}`;
100
56
  sessionStorage.setItem(SESSION_KEY, id);
57
+ sessionStorage.setItem(SESSION_TS_KEY, String(Date.now()));
101
58
  return id;
59
+ } catch {
60
+ return `sess_${generateUUID()}`;
102
61
  }
103
62
  }
104
63
 
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");
64
+ // src/helpers/http-client.ts
65
+ var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
66
+ var HttpClient = class {
67
+ constructor(apiUrl, apiKey, maxRetries = 3) {
68
+ this.apiUrl = apiUrl;
69
+ this.apiKey = apiKey;
70
+ this.maxRetries = maxRetries;
114
71
  }
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) => {
125
- 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 })
129
- };
130
- };
131
-
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()
144
- });
145
- };
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;
72
+ headers(method = "POST") {
73
+ const h = {
74
+ Authorization: `Bearer ${this.apiKey}`,
75
+ "x-identity-id": getOrCreateIdentityId(),
76
+ "x-session-id": getOrCreateSessionId()
155
77
  };
156
- window.addEventListener("popstate", () => changePage());
157
- changePage();
158
- };
159
- return init();
160
- }
161
-
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
196
- }
197
- );
198
- }
199
-
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);
205
- }
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
217
- });
218
- const mounted = (0, import_hooks.useRef)(true);
219
- async function fetchData(initial = false) {
78
+ if (method !== "GET") h["Content-Type"] = "application/json";
79
+ return h;
80
+ }
81
+ async post(path, body, attempt = 0) {
82
+ const url = `${this.apiUrl}${path}`;
220
83
  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
84
+ const res = await fetch(url, {
85
+ method: "POST",
86
+ headers: this.headers("POST"),
87
+ body: JSON.stringify(body),
88
+ keepalive: true,
89
+ credentials: "omit"
246
90
  });
91
+ if (res.status === 401) return null;
92
+ if ((res.status >= 500 || res.status === 429) && attempt < this.maxRetries) {
93
+ const jitter = 0.85 + Math.random() * 0.3;
94
+ await delay(500 * 2 ** attempt * jitter);
95
+ return this.post(path, body, attempt + 1);
96
+ }
97
+ if (res.status >= 200 && res.status < 300) {
98
+ try {
99
+ const json = await res.json();
100
+ return json?.data ?? null;
101
+ } catch {
102
+ return null;
103
+ }
104
+ }
105
+ return null;
106
+ } catch (err) {
107
+ if (err instanceof TypeError && attempt < this.maxRetries) {
108
+ const jitter = 0.85 + Math.random() * 0.3;
109
+ await delay(500 * 2 ** attempt * jitter);
110
+ return this.post(path, body, attempt + 1);
111
+ }
112
+ return null;
247
113
  }
248
114
  }
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
115
+ async get(path, attempt = 0) {
116
+ const url = `${this.apiUrl}${path}`;
117
+ try {
118
+ const res = await fetch(url, {
119
+ method: "GET",
120
+ headers: this.headers("GET"),
121
+ credentials: "omit"
259
122
  });
260
- if (isStale) {
261
- fetchData(false);
123
+ if (res.status === 401 || res.status === 404) return null;
124
+ if ((res.status >= 500 || res.status === 429) && 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);
262
128
  }
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);
129
+ if (res.status >= 200 && res.status < 300) {
130
+ try {
131
+ const json = await res.json();
132
+ return json?.data ?? null;
133
+ } catch {
134
+ return null;
135
+ }
136
+ }
137
+ return null;
138
+ } catch (err) {
139
+ if (err instanceof TypeError && attempt < this.maxRetries) {
140
+ const jitter = 0.85 + Math.random() * 0.3;
141
+ await delay(500 * 2 ** attempt * jitter);
142
+ return this.get(path, attempt + 1);
143
+ }
144
+ return null;
276
145
  }
277
- return () => {
278
- clearInterval(interval);
279
- };
280
- }, [revalidateTime]);
281
- return {
282
- ...state,
283
- refetch: () => fetchData(false)
284
- };
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;
295
146
  }
296
- const container = document.createElement("div");
297
- container.id = id;
298
- target.appendChild(container);
299
- return container;
300
- }
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);
147
+ beacon(path, body) {
148
+ if (typeof navigator === "undefined" || !navigator.sendBeacon) return false;
149
+ try {
150
+ const blob = new Blob([JSON.stringify(body)], {
151
+ type: "application/json"
152
+ });
153
+ return navigator.sendBeacon(`${this.apiUrl}${path}`, blob);
154
+ } catch {
155
+ return false;
318
156
  }
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();
329
- }
157
+ }
158
+ send(path, body, useBeacon = false) {
159
+ if (useBeacon) {
160
+ const sent = this.beacon(path, body);
161
+ if (sent) return;
330
162
  }
331
- };
332
- }
163
+ void this.post(path, body);
164
+ }
165
+ };
333
166
 
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
- });
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
- }
167
+ // src/helpers/environment.ts
168
+ function isLocalhost() {
169
+ if (typeof window === "undefined") return false;
170
+ const { hostname } = window.location;
171
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "0.0.0.0" || hostname.endsWith(".local");
172
+ }
173
+ function isOptedOut() {
174
+ try {
175
+ return localStorage.getItem("plugeen_opt_out") === "true";
176
+ } catch {
177
+ return false;
178
+ }
179
+ }
180
+ function detectBot() {
181
+ if (typeof navigator === "undefined") return false;
182
+ const ua = navigator.userAgent || "";
183
+ return Boolean(
184
+ navigator.webdriver || /HeadlessChrome/i.test(ua) || /PhantomJS/i.test(ua) || typeof window !== "undefined" && window._phantom
519
185
  );
520
186
  }
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
527
- });
528
- return render();
187
+
188
+ // src/helpers/should-skip.ts
189
+ function matchesSkipPattern(patterns) {
190
+ if (patterns.length === 0 || typeof window === "undefined") return false;
191
+ const path = window.location.pathname;
192
+ for (const pattern of patterns) {
193
+ if (pattern === path) return true;
194
+ const star = pattern.indexOf("*");
195
+ if (star !== -1 && path.startsWith(pattern.slice(0, star))) return true;
196
+ }
197
+ return false;
198
+ }
199
+ function shouldSkip(options) {
200
+ if (options.disabled) return true;
201
+ if (isOptedOut()) return true;
202
+ if (detectBot()) return true;
203
+ if (!options.debug && isLocalhost()) return true;
204
+ if (matchesSkipPattern(options.skipPatterns)) return true;
205
+ if (options.samplingRate < 1 && Math.random() > options.samplingRate)
206
+ return true;
207
+ return false;
529
208
  }
530
209
 
531
- // src/lib/plugins/events/index.ts
532
- function initEvents(api) {
210
+ // src/modules/events.ts
211
+ function createEventsModule(http, options) {
533
212
  return {
534
- create: (eventName, data) => api.post("/events", {
535
- name: eventName,
536
- data
537
- })
213
+ create: async (name, data = {}) => {
214
+ if (shouldSkip(options)) return null;
215
+ return http.post("/api/events", { name, data, source: "api" });
216
+ }
538
217
  };
539
218
  }
540
219
 
541
- // src/lib/plugins/experiments/index.ts
542
- function initExperiments(api) {
220
+ // src/modules/experiments.ts
221
+ function createExperimentsModule(http, options) {
543
222
  return {
544
- get: (experimentId) => api.get(`/plugins/experiments/${experimentId}`)
223
+ get: (id) => {
224
+ if (shouldSkip(options)) return Promise.resolve(null);
225
+ return http.get(
226
+ `/api/v1/experiments/${encodeURIComponent(id)}`
227
+ );
228
+ }
545
229
  };
546
230
  }
547
231
 
548
- // src/lib/plugins/feature-flags/index.ts
549
- function initFeatureFlags(api) {
232
+ // src/modules/feature-flags.ts
233
+ function createFeatureFlagsModule(http, options) {
550
234
  return {
551
- get: async (flagKey) => {
552
- return api.get(
553
- `/plugins/feature-flags/${flagKey}`
235
+ get: (key) => {
236
+ if (shouldSkip(options)) return Promise.resolve(null);
237
+ return http.get(
238
+ `/api/v1/feature-flags/${encodeURIComponent(key)}`
554
239
  );
555
240
  }
556
241
  };
557
242
  }
558
243
 
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;
566
- }
244
+ // src/modules/identities.ts
245
+ function createIdentitiesModule(http, options) {
567
246
  return {
568
- // get,
569
- set
247
+ set: async (distinctId, data = {}) => {
248
+ if (shouldSkip(options)) return null;
249
+ const result = await http.post("/api/identities", {
250
+ id: distinctId,
251
+ ...data
252
+ });
253
+ setIdentityId(distinctId);
254
+ return result;
255
+ }
570
256
  };
571
257
  }
572
258
 
573
- // src/lib/plugins/log-tracing/index.ts
574
- function initLogTracing(api) {
259
+ // src/modules/logs.ts
260
+ function createLogsModule(http, options) {
575
261
  return {
576
- send: (data) => api.post("/plugins/logs", data)
262
+ send: (payload) => {
263
+ if (shouldSkip(options)) return Promise.resolve(null);
264
+ return http.post("/api/v1/logs", payload);
265
+ }
577
266
  };
578
267
  }
579
268
 
580
- // src/lib/plugins/surveys/index.ts
581
- function initSurveys(api) {
269
+ // src/modules/surveys.ts
270
+ function createSurveysModule(http, options) {
582
271
  return {
583
- submit: (data) => api.post("/plugins/surveys", data)
272
+ submit: (payload) => {
273
+ if (shouldSkip(options)) return Promise.resolve(null);
274
+ return http.post("/api/v1/surveys", payload);
275
+ }
584
276
  };
585
277
  }
586
278
 
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
- img, picture, video, canvas, svg { display: block; max-width: 100%; }
592
- input, button, textarea, select { font: inherit; }
593
- p, h1, h2, h3, h4, h5, h6 { overflow-wrap: break-word; }
594
- #root { isolation: isolate; }
595
- `;
596
- var initBaseSdk = (apiKey, options) => {
597
- const api = createApi(apiKey, options);
598
- return {
599
- events: initEvents(api),
600
- identities: initIdentities(api),
601
- featureFlags: initFeatureFlags(api),
602
- logs: initLogTracing(api),
603
- surveys: initSurveys(api),
604
- experiments: initExperiments(api)
279
+ // src/plugins/analytics.ts
280
+ function initAnalyticsPlugin(plugeen, http) {
281
+ if (typeof window === "undefined") return;
282
+ let pageStartTime = Date.now();
283
+ let maxScrollDepth = 0;
284
+ let interactionCount = 0;
285
+ let pageCount = 0;
286
+ let currentUrl = window.location.href;
287
+ const updateScrollDepth = () => {
288
+ const scrollY = window.scrollY;
289
+ const { scrollHeight, clientHeight } = document.documentElement;
290
+ const available = scrollHeight - clientHeight;
291
+ if (available <= 0) return;
292
+ const depth = Math.min(100, Math.round(scrollY / available * 100));
293
+ if (depth > maxScrollDepth) maxScrollDepth = depth;
605
294
  };
606
- };
607
- function initClientSdk(apiKey, options) {
608
- const isBrowser = typeof window !== "undefined";
609
- const api = createApi(apiKey, options);
610
- if (isBrowser) {
611
- const style = document.createElement("style");
612
- style.innerHTML = reset;
613
- document.head.appendChild(style);
614
- if (options.plugins.includes("chats")) {
615
- initChat(api, options);
295
+ window.addEventListener("scroll", updateScrollDepth, { passive: true });
296
+ const countInteraction = () => {
297
+ interactionCount++;
298
+ };
299
+ for (const evt of ["mousedown", "keydown", "touchstart"]) {
300
+ window.addEventListener(evt, countInteraction, { passive: true });
301
+ }
302
+ const trackPageView = () => {
303
+ pageCount++;
304
+ const body = {
305
+ event: "page_view",
306
+ url: window.location.href,
307
+ title: document.title,
308
+ sessionId: getOrCreateSessionId(),
309
+ referrer: document.referrer || void 0,
310
+ screenWidth: window.innerWidth,
311
+ screenHeight: window.innerHeight
312
+ };
313
+ void http.post("/api/v1/analytics", body);
314
+ };
315
+ const buildPageExitData = () => ({
316
+ url: currentUrl,
317
+ time_on_page: Math.round((Date.now() - pageStartTime) / 1e3),
318
+ scroll_depth: maxScrollDepth,
319
+ interaction_count: interactionCount,
320
+ page_count: pageCount
321
+ });
322
+ const trackPageExit = (useBeacon = false) => {
323
+ const data = buildPageExitData();
324
+ if (useBeacon) {
325
+ http.send(
326
+ "/api/events",
327
+ { name: "page_exit", data, source: "api" },
328
+ true
329
+ );
330
+ } else {
331
+ void plugeen.events.create("page_exit", data);
332
+ }
333
+ };
334
+ const onRouteChange = () => {
335
+ if (window.location.href === currentUrl) return;
336
+ trackPageExit();
337
+ maxScrollDepth = 0;
338
+ interactionCount = 0;
339
+ pageStartTime = Date.now();
340
+ currentUrl = window.location.href;
341
+ trackPageView();
342
+ };
343
+ const patchHistoryMethod = (method) => {
344
+ const original = history[method].bind(history);
345
+ history[method] = (...args) => {
346
+ original(...args);
347
+ onRouteChange();
348
+ };
349
+ };
350
+ patchHistoryMethod("pushState");
351
+ patchHistoryMethod("replaceState");
352
+ window.addEventListener("popstate", onRouteChange);
353
+ window.addEventListener("beforeunload", () => {
354
+ trackPageExit(true);
355
+ });
356
+ document.addEventListener("visibilitychange", () => {
357
+ if (document.visibilityState === "hidden") {
358
+ trackPageExit(true);
359
+ }
360
+ });
361
+ trackPageView();
362
+ }
363
+
364
+ // src/plugins/errors.ts
365
+ var EXTENSION_PREFIXES = [
366
+ "chrome-extension://",
367
+ "moz-extension://",
368
+ "safari-extension://",
369
+ "edge-extension://"
370
+ ];
371
+ function isExtensionSource(str) {
372
+ if (!str) return false;
373
+ const lower = str.toLowerCase();
374
+ return EXTENSION_PREFIXES.some((prefix) => lower.includes(prefix));
375
+ }
376
+ function initErrorsPlugin(plugeen) {
377
+ if (typeof window === "undefined") return;
378
+ window.addEventListener("error", (event) => {
379
+ if (isExtensionSource(event.filename) || isExtensionSource(event.error?.stack))
380
+ return;
381
+ if (event.error === null && event.message === "Script error.") return;
382
+ void plugeen.events.create("error", {
383
+ message: event.message || "Unknown Error",
384
+ filename: event.filename,
385
+ lineno: event.lineno,
386
+ colno: event.colno,
387
+ stack: event.error?.stack,
388
+ error_type: event.error?.name || "Error"
389
+ });
390
+ });
391
+ window.addEventListener(
392
+ "unhandledrejection",
393
+ (event) => {
394
+ const { reason } = event;
395
+ if (isExtensionSource(reason?.stack))
396
+ return;
397
+ let message = "Unknown Error";
398
+ let stack;
399
+ if (reason instanceof Error) {
400
+ message = reason.message;
401
+ stack = reason.stack;
402
+ } else if (typeof reason === "string") {
403
+ message = reason;
404
+ } else if (reason !== null && typeof reason === "object" && "message" in reason) {
405
+ message = String(reason.message);
406
+ }
407
+ void plugeen.events.create("error", {
408
+ message,
409
+ stack,
410
+ error_type: "UnhandledRejection"
411
+ });
412
+ }
413
+ );
414
+ }
415
+
416
+ // src/plugins/web-vitals/helpers.ts
417
+ function getRating(value, thresholds) {
418
+ if (value > thresholds[1]) return "poor";
419
+ if (value > thresholds[0]) return "needs-improvement";
420
+ return "good";
421
+ }
422
+ function getActivationStart() {
423
+ const nav = performance.getEntriesByType(
424
+ "navigation"
425
+ )[0];
426
+ return nav?.activationStart ?? 0;
427
+ }
428
+ function getNavEntry() {
429
+ const entry = performance.getEntriesByType("navigation")[0];
430
+ if (entry && entry.responseStart > 0 && entry.responseStart < performance.now())
431
+ return entry;
432
+ }
433
+ function observe(type, cb, opts) {
434
+ try {
435
+ if (!PerformanceObserver.supportedEntryTypes.includes(type)) return;
436
+ const po = new PerformanceObserver((list) => {
437
+ Promise.resolve().then(() => cb(list.getEntries()));
438
+ });
439
+ po.observe({
440
+ type,
441
+ buffered: true,
442
+ ...opts ?? {}
443
+ });
444
+ return po;
445
+ } catch {
446
+ return void 0;
447
+ }
448
+ }
449
+ function observeFCP(cb) {
450
+ const reported = /* @__PURE__ */ new Set();
451
+ observe("paint", (entries) => {
452
+ for (const entry of entries) {
453
+ if (entry.name === "first-contentful-paint" && !reported.has("FCP")) {
454
+ reported.add("FCP");
455
+ const value = Math.max(entry.startTime - getActivationStart(), 0);
456
+ cb({
457
+ name: "FCP",
458
+ value: Math.round(value),
459
+ rating: getRating(value, [1800, 3e3])
460
+ });
461
+ }
616
462
  }
617
- if (options.plugins.includes("analytics")) {
618
- initAnalytics(api);
463
+ });
464
+ }
465
+ function observeLCP(cb) {
466
+ let reported = false;
467
+ const po = observe("largest-contentful-paint", (entries) => {
468
+ if (reported) return;
469
+ const last = entries[entries.length - 1];
470
+ if (last) {
471
+ const value = Math.max(last.startTime - getActivationStart(), 0);
472
+ cb({
473
+ name: "LCP",
474
+ value: Math.round(value),
475
+ rating: getRating(value, [2500, 4e3])
476
+ });
477
+ }
478
+ });
479
+ if (!po) return;
480
+ const finalize = () => {
481
+ if (reported) return;
482
+ reported = true;
483
+ const records = po.takeRecords();
484
+ if (records.length > 0) {
485
+ const last = records[records.length - 1];
486
+ const value = Math.max(last.startTime - getActivationStart(), 0);
487
+ cb({
488
+ name: "LCP",
489
+ value: Math.round(value),
490
+ rating: getRating(value, [2500, 4e3])
491
+ });
492
+ }
493
+ po.disconnect();
494
+ };
495
+ for (const evt of ["keydown", "click", "visibilitychange"]) {
496
+ addEventListener(evt, finalize, { capture: true, once: true });
497
+ }
498
+ }
499
+ function observeCLS(cb) {
500
+ let sessionValue = 0;
501
+ let sessionEntries = [];
502
+ let clsValue = 0;
503
+ observe("layout-shift", (entries) => {
504
+ for (const raw of entries) {
505
+ const entry = raw;
506
+ if (entry.hadRecentInput) continue;
507
+ const last = sessionEntries[sessionEntries.length - 1];
508
+ const first = sessionEntries[0];
509
+ if (sessionEntries.length > 0 && last && first && entry.startTime - last.startTime < 1e3 && entry.startTime - first.startTime < 5e3) {
510
+ sessionValue += entry.value;
511
+ sessionEntries.push(entry);
512
+ } else {
513
+ sessionValue = entry.value;
514
+ sessionEntries = [entry];
515
+ }
516
+ if (sessionValue > clsValue) {
517
+ clsValue = sessionValue;
518
+ const rounded = Math.round(clsValue * 1e4) / 1e4;
519
+ cb({
520
+ name: "CLS",
521
+ value: rounded,
522
+ rating: getRating(clsValue, [0.1, 0.25])
523
+ });
524
+ }
525
+ }
526
+ });
527
+ }
528
+ function observeTTFB(cb) {
529
+ const nav = getNavEntry();
530
+ if (!nav) return;
531
+ const value = Math.max(nav.responseStart - getActivationStart(), 0);
532
+ cb({
533
+ name: "TTFB",
534
+ value: Math.round(value),
535
+ rating: getRating(value, [800, 1800])
536
+ });
537
+ }
538
+ function observeINP(cb) {
539
+ const interactions = /* @__PURE__ */ new Map();
540
+ let worstDuration = 0;
541
+ observe(
542
+ "event",
543
+ (entries) => {
544
+ for (const raw of entries) {
545
+ const entry = raw;
546
+ if (!entry.interactionId) continue;
547
+ const existing = interactions.get(entry.interactionId) ?? 0;
548
+ if (entry.duration > existing) {
549
+ interactions.set(entry.interactionId, entry.duration);
550
+ if (entry.duration > worstDuration) {
551
+ worstDuration = entry.duration;
552
+ cb({
553
+ name: "INP",
554
+ value: Math.round(entry.duration),
555
+ rating: getRating(entry.duration, [200, 500])
556
+ });
557
+ }
558
+ }
559
+ }
560
+ },
561
+ { durationThreshold: 40 }
562
+ );
563
+ }
564
+ function observeFPS(cb) {
565
+ if (typeof requestAnimationFrame === "undefined") return;
566
+ let frames = 0;
567
+ const duration = 2e3;
568
+ const start = performance.now();
569
+ const tick = () => {
570
+ frames++;
571
+ if (performance.now() - start < duration) {
572
+ requestAnimationFrame(tick);
573
+ } else {
574
+ cb({ name: "FPS", value: Math.round(frames / duration * 1e3) });
619
575
  }
620
- return {};
576
+ };
577
+ if (document.readyState === "complete") {
578
+ requestAnimationFrame(tick);
579
+ } else {
580
+ window.addEventListener("load", () => requestAnimationFrame(tick), {
581
+ once: true
582
+ });
621
583
  }
622
- return null;
584
+ }
585
+ function initWebVitals(cb) {
586
+ if (typeof window === "undefined" || typeof PerformanceObserver === "undefined")
587
+ return;
588
+ observeFCP(cb);
589
+ observeLCP(cb);
590
+ observeCLS(cb);
591
+ observeTTFB(cb);
592
+ observeINP(cb);
593
+ observeFPS(cb);
623
594
  }
624
595
 
625
- // src/lib/index.ts
626
- var defaultOptions = {
627
- baseUrl: "https://plugeen.app/api",
628
- accentColor: "#4f46e5",
629
- foregroundColor: "#fff",
630
- plugins: []
631
- };
632
- var baseInstance = null;
633
- var clientInstance = null;
596
+ // src/plugins/web-vitals/index.ts
597
+ function initWebVitalsPlugin(plugeen) {
598
+ initWebVitals((metric) => {
599
+ void plugeen.events.create("web_vital", {
600
+ name: metric.name,
601
+ value: metric.value,
602
+ ...metric.rating !== void 0 ? { rating: metric.rating } : {}
603
+ });
604
+ });
605
+ }
606
+
607
+ // src/plugins/index.ts
608
+ function initPlugins(plugeen, http, plugins) {
609
+ for (const plugin of plugins) {
610
+ if (plugin === "analytics") initAnalyticsPlugin(plugeen, http);
611
+ else if (plugin === "web-vitals") initWebVitalsPlugin(plugeen);
612
+ else if (plugin === "errors") initErrorsPlugin(plugeen);
613
+ }
614
+ }
615
+
616
+ // src/index.ts
634
617
  function createPlugeen(apiKey, options) {
635
- const _options = {
636
- baseUrl: options?.baseUrl || defaultOptions.baseUrl,
637
- accentColor: options?.accentColor || defaultOptions.accentColor,
638
- foregroundColor: options?.foregroundColor || defaultOptions.foregroundColor,
639
- plugins: options?.plugins || []
640
- };
641
- if (!apiKey) {
642
- console.warn("[Plugeen] Missing data-api-key attribute.");
618
+ if (!apiKey || typeof apiKey !== "string") {
619
+ throw new TypeError("[plugeen] apiKey is required");
643
620
  }
644
- if (!baseInstance) {
645
- baseInstance = initBaseSdk(apiKey, _options);
621
+ if (!options?.apiUrl || typeof options.apiUrl !== "string") {
622
+ throw new TypeError("[plugeen] apiUrl is required");
646
623
  }
647
- if (!clientInstance) {
648
- clientInstance = initClientSdk(apiKey, _options);
624
+ const resolved = {
625
+ apiUrl: options.apiUrl,
626
+ debug: options.debug ?? false,
627
+ disabled: options.disabled ?? false,
628
+ samplingRate: options.samplingRate ?? 1,
629
+ plugins: options.plugins ?? [],
630
+ skipPatterns: options.skipPatterns ?? []
631
+ };
632
+ const http = new HttpClient(resolved.apiUrl, apiKey);
633
+ const events = createEventsModule(http, resolved);
634
+ const identities = createIdentitiesModule(http, resolved);
635
+ const featureFlags = createFeatureFlagsModule(http, resolved);
636
+ const logs = createLogsModule(http, resolved);
637
+ const surveys = createSurveysModule(http, resolved);
638
+ const experiments = createExperimentsModule(http, resolved);
639
+ const plugeen = {
640
+ track: (event, properties) => {
641
+ void events.create(event, properties ?? {});
642
+ },
643
+ events,
644
+ identities,
645
+ featureFlags,
646
+ logs,
647
+ surveys,
648
+ experiments
649
+ };
650
+ if (resolved.plugins.length > 0) {
651
+ initPlugins(plugeen, http, resolved.plugins);
649
652
  }
650
- return baseInstance;
653
+ return plugeen;
651
654
  }
652
- // Annotate the CommonJS export names for ESM import in node:
653
- 0 && (module.exports = {
654
- createPlugeen
655
- });
655
+
656
+ exports.createPlugeen = createPlugeen;