plugeen 0.0.8 → 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
@@ -24,6 +24,7 @@ var IDENTITY_KEY = "plugeen_anon_id";
24
24
  var SESSION_KEY = "plugeen_session_id";
25
25
  var SESSION_TS_KEY = "plugeen_session_ts";
26
26
  var SESSION_TTL = 18e5;
27
+ var memStore = {};
27
28
  function getOrCreateIdentityId() {
28
29
  try {
29
30
  let id = localStorage.getItem(IDENTITY_KEY);
@@ -33,13 +34,17 @@ function getOrCreateIdentityId() {
33
34
  }
34
35
  return id;
35
36
  } catch {
36
- return `anon_${generateUUID()}`;
37
+ if (!memStore[IDENTITY_KEY]) {
38
+ memStore[IDENTITY_KEY] = `anon_${generateUUID()}`;
39
+ }
40
+ return memStore[IDENTITY_KEY];
37
41
  }
38
42
  }
39
43
  function setIdentityId(id) {
40
44
  try {
41
45
  localStorage.setItem(IDENTITY_KEY, id);
42
46
  } catch {
47
+ memStore[IDENTITY_KEY] = id;
43
48
  }
44
49
  }
45
50
  function getOrCreateSessionId() {
@@ -57,19 +62,28 @@ function getOrCreateSessionId() {
57
62
  sessionStorage.setItem(SESSION_TS_KEY, String(Date.now()));
58
63
  return id;
59
64
  } catch {
60
- return `sess_${generateUUID()}`;
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());
74
+ return id;
61
75
  }
62
76
  }
63
77
 
64
- // src/helpers/http-client.ts
78
+ // src/helpers/http-client/index.ts
65
79
  var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
66
80
  var HttpClient = class {
67
- constructor(apiUrl, apiKey, maxRetries = 3) {
68
- this.apiUrl = apiUrl;
81
+ constructor(baseUrl, apiKey, maxRetries = 3) {
82
+ this.baseUrl = baseUrl;
69
83
  this.apiKey = apiKey;
70
84
  this.maxRetries = maxRetries;
71
85
  }
72
- headers(method = "POST") {
86
+ headers(method) {
73
87
  const h = {
74
88
  Authorization: `Bearer ${this.apiKey}`,
75
89
  "x-identity-id": getOrCreateIdentityId(),
@@ -78,71 +92,55 @@ var HttpClient = class {
78
92
  if (method !== "GET") h["Content-Type"] = "application/json";
79
93
  return h;
80
94
  }
81
- async post(path, body, attempt = 0) {
82
- const url = `${this.apiUrl}${path}`;
95
+ async request(path, { attempt = 0, method, body }) {
96
+ const url = `${this.baseUrl}${path}`;
83
97
  try {
84
98
  const res = await fetch(url, {
85
- method: "POST",
86
- headers: this.headers("POST"),
87
- body: JSON.stringify(body),
88
- keepalive: true,
89
- credentials: "omit"
99
+ method,
100
+ headers: this.headers(method),
101
+ credentials: "omit",
102
+ body: body ? JSON.stringify(body) : void 0
90
103
  });
91
- if (res.status === 401) return null;
104
+ if (res.status === 401 || res.status === 404) return [null, res.status];
92
105
  if ((res.status >= 500 || res.status === 429) && attempt < this.maxRetries) {
93
106
  const jitter = 0.85 + Math.random() * 0.3;
94
107
  await delay(500 * 2 ** attempt * jitter);
95
- return this.post(path, body, attempt + 1);
108
+ return this.request(path, {
109
+ body,
110
+ attempt: attempt + 1,
111
+ method
112
+ });
96
113
  }
97
114
  if (res.status >= 200 && res.status < 300) {
98
115
  try {
99
116
  const json = await res.json();
100
- return json?.data ?? null;
117
+ return [json.data, null];
101
118
  } catch {
102
- return null;
119
+ return [null, "Failed to parse json"];
103
120
  }
104
121
  }
105
- return null;
122
+ return [null, "Error"];
106
123
  } catch (err) {
107
124
  if (err instanceof TypeError && attempt < this.maxRetries) {
108
125
  const jitter = 0.85 + Math.random() * 0.3;
109
126
  await delay(500 * 2 ** attempt * jitter);
110
- return this.post(path, body, attempt + 1);
127
+ return this.get(path, attempt + 1);
111
128
  }
112
- return null;
129
+ return [null, err];
113
130
  }
114
131
  }
132
+ async post(path, body, attempt = 0) {
133
+ return this.request(path, {
134
+ attempt,
135
+ body,
136
+ method: "POST"
137
+ });
138
+ }
115
139
  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"
122
- });
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);
128
- }
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;
145
- }
140
+ return this.request(path, {
141
+ attempt,
142
+ method: "GET"
143
+ });
146
144
  }
147
145
  beacon(path, body) {
148
146
  if (typeof navigator === "undefined" || !navigator.sendBeacon) return false;
@@ -150,103 +148,27 @@ var HttpClient = class {
150
148
  const blob = new Blob([JSON.stringify(body)], {
151
149
  type: "application/json"
152
150
  });
153
- return navigator.sendBeacon(`${this.apiUrl}${path}`, blob);
151
+ return navigator.sendBeacon(`${this.baseUrl}${path}`, blob);
154
152
  } catch {
155
153
  return false;
156
154
  }
157
155
  }
158
- send(path, body, useBeacon = false) {
159
- if (useBeacon) {
160
- const sent = this.beacon(path, body);
161
- if (sent) return;
162
- }
163
- void this.post(path, body);
164
- }
165
156
  };
166
157
 
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
185
- );
186
- }
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;
208
- }
209
-
210
- // src/modules/events.ts
211
- function createEventsModule(http, options) {
158
+ // src/plugins/events/index.ts
159
+ function createEventsModule(http) {
212
160
  return {
213
161
  create: async (name, data = {}) => {
214
- if (shouldSkip(options)) return null;
215
- return http.post("/api/events", { name, data, source: "api" });
216
- }
217
- };
218
- }
219
-
220
- // src/modules/experiments.ts
221
- function createExperimentsModule(http, options) {
222
- return {
223
- get: (id) => {
224
- if (shouldSkip(options)) return Promise.resolve(null);
225
- return http.get(
226
- `/api/v1/experiments/${encodeURIComponent(id)}`
227
- );
162
+ return http.post("/events", { name, data, source: "api" });
228
163
  }
229
164
  };
230
165
  }
231
166
 
232
- // src/modules/feature-flags.ts
233
- function createFeatureFlagsModule(http, options) {
167
+ // src/plugins/identities/index.ts
168
+ function createIdentitiesModule(http) {
234
169
  return {
235
- get: (key) => {
236
- if (shouldSkip(options)) return Promise.resolve(null);
237
- return http.get(
238
- `/api/v1/feature-flags/${encodeURIComponent(key)}`
239
- );
240
- }
241
- };
242
- }
243
-
244
- // src/modules/identities.ts
245
- function createIdentitiesModule(http, options) {
246
- return {
247
- set: async (distinctId, data = {}) => {
248
- if (shouldSkip(options)) return null;
249
- const result = await http.post("/api/identities", {
170
+ create: async (distinctId, data = {}) => {
171
+ const result = await http.post("/identities", {
250
172
  id: distinctId,
251
173
  ...data
252
174
  });
@@ -256,52 +178,15 @@ function createIdentitiesModule(http, options) {
256
178
  };
257
179
  }
258
180
 
259
- // src/modules/logs.ts
260
- function createLogsModule(http, options) {
261
- return {
262
- send: (payload) => {
263
- if (shouldSkip(options)) return Promise.resolve(null);
264
- return http.post("/api/v1/logs", payload);
265
- }
266
- };
267
- }
268
-
269
- // src/modules/surveys.ts
270
- function createSurveysModule(http, options) {
271
- return {
272
- submit: (payload) => {
273
- if (shouldSkip(options)) return Promise.resolve(null);
274
- return http.post("/api/v1/surveys", payload);
275
- }
276
- };
277
- }
278
-
279
- // src/plugins/analytics.ts
280
- function initAnalyticsPlugin(plugeen, http) {
181
+ // src/plugins/analytics/index.ts
182
+ function initAnalyticsPlugin(http) {
281
183
  if (typeof window === "undefined") return;
282
184
  let pageStartTime = Date.now();
283
- let maxScrollDepth = 0;
284
- let interactionCount = 0;
285
185
  let pageCount = 0;
286
186
  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;
294
- };
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
187
  const trackPageView = () => {
303
188
  pageCount++;
304
- const body = {
189
+ void http.post("/v1/analytics", {
305
190
  event: "page_view",
306
191
  url: window.location.href,
307
192
  title: document.title,
@@ -309,33 +194,31 @@ function initAnalyticsPlugin(plugeen, http) {
309
194
  referrer: document.referrer || void 0,
310
195
  screenWidth: window.innerWidth,
311
196
  screenHeight: window.innerHeight
312
- };
313
- void http.post("/api/v1/analytics", body);
197
+ });
314
198
  };
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
199
  const trackPageExit = (useBeacon = false) => {
323
- const data = buildPageExitData();
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
+ }
212
+ };
324
213
  if (useBeacon) {
325
- http.send(
326
- "/api/events",
327
- { name: "page_exit", data, source: "api" },
328
- true
329
- );
214
+ http.beacon("/v1/analytics", data);
330
215
  } else {
331
- void plugeen.events.create("page_exit", data);
216
+ void http.post("/v1/analytics", data);
332
217
  }
333
218
  };
334
219
  const onRouteChange = () => {
335
220
  if (window.location.href === currentUrl) return;
336
221
  trackPageExit();
337
- maxScrollDepth = 0;
338
- interactionCount = 0;
339
222
  pageStartTime = Date.now();
340
223
  currentUrl = window.location.href;
341
224
  trackPageView();
@@ -350,9 +233,7 @@ function initAnalyticsPlugin(plugeen, http) {
350
233
  patchHistoryMethod("pushState");
351
234
  patchHistoryMethod("replaceState");
352
235
  window.addEventListener("popstate", onRouteChange);
353
- window.addEventListener("beforeunload", () => {
354
- trackPageExit(true);
355
- });
236
+ window.addEventListener("beforeunload", () => trackPageExit(true));
356
237
  document.addEventListener("visibilitychange", () => {
357
238
  if (document.visibilityState === "hidden") {
358
239
  trackPageExit(true);
@@ -361,7 +242,7 @@ function initAnalyticsPlugin(plugeen, http) {
361
242
  trackPageView();
362
243
  }
363
244
 
364
- // src/plugins/errors.ts
245
+ // src/plugins/logs/index.ts
365
246
  var EXTENSION_PREFIXES = [
366
247
  "chrome-extension://",
367
248
  "moz-extension://",
@@ -373,13 +254,13 @@ function isExtensionSource(str) {
373
254
  const lower = str.toLowerCase();
374
255
  return EXTENSION_PREFIXES.some((prefix) => lower.includes(prefix));
375
256
  }
376
- function initErrorsPlugin(plugeen) {
257
+ function initErrorsPlugin(http) {
377
258
  if (typeof window === "undefined") return;
378
259
  window.addEventListener("error", (event) => {
379
260
  if (isExtensionSource(event.filename) || isExtensionSource(event.error?.stack))
380
261
  return;
381
262
  if (event.error === null && event.message === "Script error.") return;
382
- void plugeen.events.create("error", {
263
+ void http.post("/v1/logs", {
383
264
  message: event.message || "Unknown Error",
384
265
  filename: event.filename,
385
266
  lineno: event.lineno,
@@ -404,7 +285,7 @@ function initErrorsPlugin(plugeen) {
404
285
  } else if (reason !== null && typeof reason === "object" && "message" in reason) {
405
286
  message = String(reason.message);
406
287
  }
407
- void plugeen.events.create("error", {
288
+ void http.post("/v1/logs", {
408
289
  message,
409
290
  stack,
410
291
  error_type: "UnhandledRejection"
@@ -594,9 +475,9 @@ function initWebVitals(cb) {
594
475
  }
595
476
 
596
477
  // src/plugins/web-vitals/index.ts
597
- function initWebVitalsPlugin(plugeen) {
478
+ function initWebVitalsPlugin(http) {
598
479
  initWebVitals((metric) => {
599
- void plugeen.events.create("web_vital", {
480
+ http.post("/v1/web-vitals", {
600
481
  name: metric.name,
601
482
  value: metric.value,
602
483
  ...metric.rating !== void 0 ? { rating: metric.rating } : {}
@@ -605,50 +486,41 @@ function initWebVitalsPlugin(plugeen) {
605
486
  }
606
487
 
607
488
  // 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
- }
489
+ function initPlugins(http, plugins) {
490
+ const initializers = {
491
+ analytics: initAnalyticsPlugin(http),
492
+ "web-vitals": initWebVitalsPlugin(http),
493
+ errors: initErrorsPlugin(http)
494
+ };
495
+ plugins.map((p) => initializers[p]);
614
496
  }
615
497
 
616
498
  // src/index.ts
499
+ var defaults = {
500
+ baseUrl: "https://dev.plugeen.app/api",
501
+ debug: false,
502
+ plugins: []
503
+ };
617
504
  function createPlugeen(apiKey, options) {
618
- if (!apiKey || typeof apiKey !== "string") {
619
- throw new TypeError("[plugeen] apiKey is required");
620
- }
621
- if (!options?.apiUrl || typeof options.apiUrl !== "string") {
622
- throw new TypeError("[plugeen] apiUrl is required");
623
- }
624
505
  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 ?? []
506
+ baseUrl: options.baseUrl || defaults.baseUrl,
507
+ debug: options.debug || defaults.debug,
508
+ plugins: options.plugins || defaults.plugins
631
509
  };
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);
510
+ if (!apiKey) {
511
+ throw new TypeError("[plugeen] apiKey is required");
512
+ }
513
+ const http = new HttpClient(resolved.baseUrl, apiKey);
514
+ const events = createEventsModule(http);
515
+ const identities = createIdentitiesModule(http);
639
516
  const plugeen = {
640
517
  track: (event, properties) => {
641
- void events.create(event, properties ?? {});
518
+ void events.create(event, properties);
642
519
  },
643
- events,
644
- identities,
645
- featureFlags,
646
- logs,
647
- surveys,
648
- experiments
520
+ identify: (userId, data) => identities.create(userId, data)
649
521
  };
650
522
  if (resolved.plugins.length > 0) {
651
- initPlugins(plugeen, http, resolved.plugins);
523
+ initPlugins(http, resolved.plugins);
652
524
  }
653
525
  return plugeen;
654
526
  }
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@ var IDENTITY_KEY = "plugeen_anon_id";
22
22
  var SESSION_KEY = "plugeen_session_id";
23
23
  var SESSION_TS_KEY = "plugeen_session_ts";
24
24
  var SESSION_TTL = 18e5;
25
+ var memStore = {};
25
26
  function getOrCreateIdentityId() {
26
27
  try {
27
28
  let id = localStorage.getItem(IDENTITY_KEY);
@@ -31,13 +32,17 @@ function getOrCreateIdentityId() {
31
32
  }
32
33
  return id;
33
34
  } catch {
34
- return `anon_${generateUUID()}`;
35
+ if (!memStore[IDENTITY_KEY]) {
36
+ memStore[IDENTITY_KEY] = `anon_${generateUUID()}`;
37
+ }
38
+ return memStore[IDENTITY_KEY];
35
39
  }
36
40
  }
37
41
  function setIdentityId(id) {
38
42
  try {
39
43
  localStorage.setItem(IDENTITY_KEY, id);
40
44
  } catch {
45
+ memStore[IDENTITY_KEY] = id;
41
46
  }
42
47
  }
43
48
  function getOrCreateSessionId() {
@@ -55,19 +60,28 @@ function getOrCreateSessionId() {
55
60
  sessionStorage.setItem(SESSION_TS_KEY, String(Date.now()));
56
61
  return id;
57
62
  } catch {
58
- return `sess_${generateUUID()}`;
63
+ const existing = memStore[SESSION_KEY];
64
+ const ts = memStore[SESSION_TS_KEY];
65
+ if (existing && ts && Date.now() - parseInt(ts, 10) < SESSION_TTL) {
66
+ memStore[SESSION_TS_KEY] = String(Date.now());
67
+ return existing;
68
+ }
69
+ const id = `sess_${generateUUID()}`;
70
+ memStore[SESSION_KEY] = id;
71
+ memStore[SESSION_TS_KEY] = String(Date.now());
72
+ return id;
59
73
  }
60
74
  }
61
75
 
62
- // src/helpers/http-client.ts
76
+ // src/helpers/http-client/index.ts
63
77
  var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
64
78
  var HttpClient = class {
65
- constructor(apiUrl, apiKey, maxRetries = 3) {
66
- this.apiUrl = apiUrl;
79
+ constructor(baseUrl, apiKey, maxRetries = 3) {
80
+ this.baseUrl = baseUrl;
67
81
  this.apiKey = apiKey;
68
82
  this.maxRetries = maxRetries;
69
83
  }
70
- headers(method = "POST") {
84
+ headers(method) {
71
85
  const h = {
72
86
  Authorization: `Bearer ${this.apiKey}`,
73
87
  "x-identity-id": getOrCreateIdentityId(),
@@ -76,71 +90,55 @@ var HttpClient = class {
76
90
  if (method !== "GET") h["Content-Type"] = "application/json";
77
91
  return h;
78
92
  }
79
- async post(path, body, attempt = 0) {
80
- const url = `${this.apiUrl}${path}`;
93
+ async request(path, { attempt = 0, method, body }) {
94
+ const url = `${this.baseUrl}${path}`;
81
95
  try {
82
96
  const res = await fetch(url, {
83
- method: "POST",
84
- headers: this.headers("POST"),
85
- body: JSON.stringify(body),
86
- keepalive: true,
87
- credentials: "omit"
97
+ method,
98
+ headers: this.headers(method),
99
+ credentials: "omit",
100
+ body: body ? JSON.stringify(body) : void 0
88
101
  });
89
- if (res.status === 401) return null;
102
+ if (res.status === 401 || res.status === 404) return [null, res.status];
90
103
  if ((res.status >= 500 || res.status === 429) && attempt < this.maxRetries) {
91
104
  const jitter = 0.85 + Math.random() * 0.3;
92
105
  await delay(500 * 2 ** attempt * jitter);
93
- return this.post(path, body, attempt + 1);
106
+ return this.request(path, {
107
+ body,
108
+ attempt: attempt + 1,
109
+ method
110
+ });
94
111
  }
95
112
  if (res.status >= 200 && res.status < 300) {
96
113
  try {
97
114
  const json = await res.json();
98
- return json?.data ?? null;
115
+ return [json.data, null];
99
116
  } catch {
100
- return null;
117
+ return [null, "Failed to parse json"];
101
118
  }
102
119
  }
103
- return null;
120
+ return [null, "Error"];
104
121
  } catch (err) {
105
122
  if (err instanceof TypeError && attempt < this.maxRetries) {
106
123
  const jitter = 0.85 + Math.random() * 0.3;
107
124
  await delay(500 * 2 ** attempt * jitter);
108
- return this.post(path, body, attempt + 1);
125
+ return this.get(path, attempt + 1);
109
126
  }
110
- return null;
127
+ return [null, err];
111
128
  }
112
129
  }
130
+ async post(path, body, attempt = 0) {
131
+ return this.request(path, {
132
+ attempt,
133
+ body,
134
+ method: "POST"
135
+ });
136
+ }
113
137
  async get(path, attempt = 0) {
114
- const url = `${this.apiUrl}${path}`;
115
- try {
116
- const res = await fetch(url, {
117
- method: "GET",
118
- headers: this.headers("GET"),
119
- credentials: "omit"
120
- });
121
- if (res.status === 401 || res.status === 404) return null;
122
- if ((res.status >= 500 || res.status === 429) && attempt < this.maxRetries) {
123
- const jitter = 0.85 + Math.random() * 0.3;
124
- await delay(500 * 2 ** attempt * jitter);
125
- return this.get(path, attempt + 1);
126
- }
127
- if (res.status >= 200 && res.status < 300) {
128
- try {
129
- const json = await res.json();
130
- return json?.data ?? null;
131
- } catch {
132
- return null;
133
- }
134
- }
135
- return null;
136
- } catch (err) {
137
- if (err instanceof TypeError && attempt < this.maxRetries) {
138
- const jitter = 0.85 + Math.random() * 0.3;
139
- await delay(500 * 2 ** attempt * jitter);
140
- return this.get(path, attempt + 1);
141
- }
142
- return null;
143
- }
138
+ return this.request(path, {
139
+ attempt,
140
+ method: "GET"
141
+ });
144
142
  }
145
143
  beacon(path, body) {
146
144
  if (typeof navigator === "undefined" || !navigator.sendBeacon) return false;
@@ -148,103 +146,27 @@ var HttpClient = class {
148
146
  const blob = new Blob([JSON.stringify(body)], {
149
147
  type: "application/json"
150
148
  });
151
- return navigator.sendBeacon(`${this.apiUrl}${path}`, blob);
149
+ return navigator.sendBeacon(`${this.baseUrl}${path}`, blob);
152
150
  } catch {
153
151
  return false;
154
152
  }
155
153
  }
156
- send(path, body, useBeacon = false) {
157
- if (useBeacon) {
158
- const sent = this.beacon(path, body);
159
- if (sent) return;
160
- }
161
- void this.post(path, body);
162
- }
163
154
  };
164
155
 
165
- // src/helpers/environment.ts
166
- function isLocalhost() {
167
- if (typeof window === "undefined") return false;
168
- const { hostname } = window.location;
169
- return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "0.0.0.0" || hostname.endsWith(".local");
170
- }
171
- function isOptedOut() {
172
- try {
173
- return localStorage.getItem("plugeen_opt_out") === "true";
174
- } catch {
175
- return false;
176
- }
177
- }
178
- function detectBot() {
179
- if (typeof navigator === "undefined") return false;
180
- const ua = navigator.userAgent || "";
181
- return Boolean(
182
- navigator.webdriver || /HeadlessChrome/i.test(ua) || /PhantomJS/i.test(ua) || typeof window !== "undefined" && window._phantom
183
- );
184
- }
185
-
186
- // src/helpers/should-skip.ts
187
- function matchesSkipPattern(patterns) {
188
- if (patterns.length === 0 || typeof window === "undefined") return false;
189
- const path = window.location.pathname;
190
- for (const pattern of patterns) {
191
- if (pattern === path) return true;
192
- const star = pattern.indexOf("*");
193
- if (star !== -1 && path.startsWith(pattern.slice(0, star))) return true;
194
- }
195
- return false;
196
- }
197
- function shouldSkip(options) {
198
- if (options.disabled) return true;
199
- if (isOptedOut()) return true;
200
- if (detectBot()) return true;
201
- if (!options.debug && isLocalhost()) return true;
202
- if (matchesSkipPattern(options.skipPatterns)) return true;
203
- if (options.samplingRate < 1 && Math.random() > options.samplingRate)
204
- return true;
205
- return false;
206
- }
207
-
208
- // src/modules/events.ts
209
- function createEventsModule(http, options) {
156
+ // src/plugins/events/index.ts
157
+ function createEventsModule(http) {
210
158
  return {
211
159
  create: async (name, data = {}) => {
212
- if (shouldSkip(options)) return null;
213
- return http.post("/api/events", { name, data, source: "api" });
214
- }
215
- };
216
- }
217
-
218
- // src/modules/experiments.ts
219
- function createExperimentsModule(http, options) {
220
- return {
221
- get: (id) => {
222
- if (shouldSkip(options)) return Promise.resolve(null);
223
- return http.get(
224
- `/api/v1/experiments/${encodeURIComponent(id)}`
225
- );
160
+ return http.post("/events", { name, data, source: "api" });
226
161
  }
227
162
  };
228
163
  }
229
164
 
230
- // src/modules/feature-flags.ts
231
- function createFeatureFlagsModule(http, options) {
165
+ // src/plugins/identities/index.ts
166
+ function createIdentitiesModule(http) {
232
167
  return {
233
- get: (key) => {
234
- if (shouldSkip(options)) return Promise.resolve(null);
235
- return http.get(
236
- `/api/v1/feature-flags/${encodeURIComponent(key)}`
237
- );
238
- }
239
- };
240
- }
241
-
242
- // src/modules/identities.ts
243
- function createIdentitiesModule(http, options) {
244
- return {
245
- set: async (distinctId, data = {}) => {
246
- if (shouldSkip(options)) return null;
247
- const result = await http.post("/api/identities", {
168
+ create: async (distinctId, data = {}) => {
169
+ const result = await http.post("/identities", {
248
170
  id: distinctId,
249
171
  ...data
250
172
  });
@@ -254,52 +176,15 @@ function createIdentitiesModule(http, options) {
254
176
  };
255
177
  }
256
178
 
257
- // src/modules/logs.ts
258
- function createLogsModule(http, options) {
259
- return {
260
- send: (payload) => {
261
- if (shouldSkip(options)) return Promise.resolve(null);
262
- return http.post("/api/v1/logs", payload);
263
- }
264
- };
265
- }
266
-
267
- // src/modules/surveys.ts
268
- function createSurveysModule(http, options) {
269
- return {
270
- submit: (payload) => {
271
- if (shouldSkip(options)) return Promise.resolve(null);
272
- return http.post("/api/v1/surveys", payload);
273
- }
274
- };
275
- }
276
-
277
- // src/plugins/analytics.ts
278
- function initAnalyticsPlugin(plugeen, http) {
179
+ // src/plugins/analytics/index.ts
180
+ function initAnalyticsPlugin(http) {
279
181
  if (typeof window === "undefined") return;
280
182
  let pageStartTime = Date.now();
281
- let maxScrollDepth = 0;
282
- let interactionCount = 0;
283
183
  let pageCount = 0;
284
184
  let currentUrl = window.location.href;
285
- const updateScrollDepth = () => {
286
- const scrollY = window.scrollY;
287
- const { scrollHeight, clientHeight } = document.documentElement;
288
- const available = scrollHeight - clientHeight;
289
- if (available <= 0) return;
290
- const depth = Math.min(100, Math.round(scrollY / available * 100));
291
- if (depth > maxScrollDepth) maxScrollDepth = depth;
292
- };
293
- window.addEventListener("scroll", updateScrollDepth, { passive: true });
294
- const countInteraction = () => {
295
- interactionCount++;
296
- };
297
- for (const evt of ["mousedown", "keydown", "touchstart"]) {
298
- window.addEventListener(evt, countInteraction, { passive: true });
299
- }
300
185
  const trackPageView = () => {
301
186
  pageCount++;
302
- const body = {
187
+ void http.post("/v1/analytics", {
303
188
  event: "page_view",
304
189
  url: window.location.href,
305
190
  title: document.title,
@@ -307,33 +192,31 @@ function initAnalyticsPlugin(plugeen, http) {
307
192
  referrer: document.referrer || void 0,
308
193
  screenWidth: window.innerWidth,
309
194
  screenHeight: window.innerHeight
310
- };
311
- void http.post("/api/v1/analytics", body);
195
+ });
312
196
  };
313
- const buildPageExitData = () => ({
314
- url: currentUrl,
315
- time_on_page: Math.round((Date.now() - pageStartTime) / 1e3),
316
- scroll_depth: maxScrollDepth,
317
- interaction_count: interactionCount,
318
- page_count: pageCount
319
- });
320
197
  const trackPageExit = (useBeacon = false) => {
321
- const data = buildPageExitData();
198
+ const data = {
199
+ event: "page_exit",
200
+ url: window.location.href,
201
+ title: document.title,
202
+ sessionId: getOrCreateSessionId(),
203
+ referrer: document.referrer || void 0,
204
+ screenWidth: window.innerWidth,
205
+ screenHeight: window.innerHeight,
206
+ metadata: {
207
+ timeOnPage: Math.round((Date.now() - pageStartTime) / 1e3),
208
+ pageCount
209
+ }
210
+ };
322
211
  if (useBeacon) {
323
- http.send(
324
- "/api/events",
325
- { name: "page_exit", data, source: "api" },
326
- true
327
- );
212
+ http.beacon("/v1/analytics", data);
328
213
  } else {
329
- void plugeen.events.create("page_exit", data);
214
+ void http.post("/v1/analytics", data);
330
215
  }
331
216
  };
332
217
  const onRouteChange = () => {
333
218
  if (window.location.href === currentUrl) return;
334
219
  trackPageExit();
335
- maxScrollDepth = 0;
336
- interactionCount = 0;
337
220
  pageStartTime = Date.now();
338
221
  currentUrl = window.location.href;
339
222
  trackPageView();
@@ -348,9 +231,7 @@ function initAnalyticsPlugin(plugeen, http) {
348
231
  patchHistoryMethod("pushState");
349
232
  patchHistoryMethod("replaceState");
350
233
  window.addEventListener("popstate", onRouteChange);
351
- window.addEventListener("beforeunload", () => {
352
- trackPageExit(true);
353
- });
234
+ window.addEventListener("beforeunload", () => trackPageExit(true));
354
235
  document.addEventListener("visibilitychange", () => {
355
236
  if (document.visibilityState === "hidden") {
356
237
  trackPageExit(true);
@@ -359,7 +240,7 @@ function initAnalyticsPlugin(plugeen, http) {
359
240
  trackPageView();
360
241
  }
361
242
 
362
- // src/plugins/errors.ts
243
+ // src/plugins/logs/index.ts
363
244
  var EXTENSION_PREFIXES = [
364
245
  "chrome-extension://",
365
246
  "moz-extension://",
@@ -371,13 +252,13 @@ function isExtensionSource(str) {
371
252
  const lower = str.toLowerCase();
372
253
  return EXTENSION_PREFIXES.some((prefix) => lower.includes(prefix));
373
254
  }
374
- function initErrorsPlugin(plugeen) {
255
+ function initErrorsPlugin(http) {
375
256
  if (typeof window === "undefined") return;
376
257
  window.addEventListener("error", (event) => {
377
258
  if (isExtensionSource(event.filename) || isExtensionSource(event.error?.stack))
378
259
  return;
379
260
  if (event.error === null && event.message === "Script error.") return;
380
- void plugeen.events.create("error", {
261
+ void http.post("/v1/logs", {
381
262
  message: event.message || "Unknown Error",
382
263
  filename: event.filename,
383
264
  lineno: event.lineno,
@@ -402,7 +283,7 @@ function initErrorsPlugin(plugeen) {
402
283
  } else if (reason !== null && typeof reason === "object" && "message" in reason) {
403
284
  message = String(reason.message);
404
285
  }
405
- void plugeen.events.create("error", {
286
+ void http.post("/v1/logs", {
406
287
  message,
407
288
  stack,
408
289
  error_type: "UnhandledRejection"
@@ -592,9 +473,9 @@ function initWebVitals(cb) {
592
473
  }
593
474
 
594
475
  // src/plugins/web-vitals/index.ts
595
- function initWebVitalsPlugin(plugeen) {
476
+ function initWebVitalsPlugin(http) {
596
477
  initWebVitals((metric) => {
597
- void plugeen.events.create("web_vital", {
478
+ http.post("/v1/web-vitals", {
598
479
  name: metric.name,
599
480
  value: metric.value,
600
481
  ...metric.rating !== void 0 ? { rating: metric.rating } : {}
@@ -603,50 +484,41 @@ function initWebVitalsPlugin(plugeen) {
603
484
  }
604
485
 
605
486
  // src/plugins/index.ts
606
- function initPlugins(plugeen, http, plugins) {
607
- for (const plugin of plugins) {
608
- if (plugin === "analytics") initAnalyticsPlugin(plugeen, http);
609
- else if (plugin === "web-vitals") initWebVitalsPlugin(plugeen);
610
- else if (plugin === "errors") initErrorsPlugin(plugeen);
611
- }
487
+ function initPlugins(http, plugins) {
488
+ const initializers = {
489
+ analytics: initAnalyticsPlugin(http),
490
+ "web-vitals": initWebVitalsPlugin(http),
491
+ errors: initErrorsPlugin(http)
492
+ };
493
+ plugins.map((p) => initializers[p]);
612
494
  }
613
495
 
614
496
  // src/index.ts
497
+ var defaults = {
498
+ baseUrl: "https://dev.plugeen.app/api",
499
+ debug: false,
500
+ plugins: []
501
+ };
615
502
  function createPlugeen(apiKey, options) {
616
- if (!apiKey || typeof apiKey !== "string") {
617
- throw new TypeError("[plugeen] apiKey is required");
618
- }
619
- if (!options?.apiUrl || typeof options.apiUrl !== "string") {
620
- throw new TypeError("[plugeen] apiUrl is required");
621
- }
622
503
  const resolved = {
623
- apiUrl: options.apiUrl,
624
- debug: options.debug ?? false,
625
- disabled: options.disabled ?? false,
626
- samplingRate: options.samplingRate ?? 1,
627
- plugins: options.plugins ?? [],
628
- skipPatterns: options.skipPatterns ?? []
504
+ baseUrl: options.baseUrl || defaults.baseUrl,
505
+ debug: options.debug || defaults.debug,
506
+ plugins: options.plugins || defaults.plugins
629
507
  };
630
- const http = new HttpClient(resolved.apiUrl, apiKey);
631
- const events = createEventsModule(http, resolved);
632
- const identities = createIdentitiesModule(http, resolved);
633
- const featureFlags = createFeatureFlagsModule(http, resolved);
634
- const logs = createLogsModule(http, resolved);
635
- const surveys = createSurveysModule(http, resolved);
636
- const experiments = createExperimentsModule(http, resolved);
508
+ if (!apiKey) {
509
+ throw new TypeError("[plugeen] apiKey is required");
510
+ }
511
+ const http = new HttpClient(resolved.baseUrl, apiKey);
512
+ const events = createEventsModule(http);
513
+ const identities = createIdentitiesModule(http);
637
514
  const plugeen = {
638
515
  track: (event, properties) => {
639
- void events.create(event, properties ?? {});
516
+ void events.create(event, properties);
640
517
  },
641
- events,
642
- identities,
643
- featureFlags,
644
- logs,
645
- surveys,
646
- experiments
518
+ identify: (userId, data) => identities.create(userId, data)
647
519
  };
648
520
  if (resolved.plugins.length > 0) {
649
- initPlugins(plugeen, http, resolved.plugins);
521
+ initPlugins(http, resolved.plugins);
650
522
  }
651
523
  return plugeen;
652
524
  }
@@ -1,2 +1,2 @@
1
1
  /* plugeen v0.1.0 | https://plugeen.app */
2
- "use strict";var plugeen=(()=>{function _(){if(typeof window>"u")return!1;let{hostname:e}=window.location;return e==="localhost"||e==="127.0.0.1"||e==="[::1]"||e==="0.0.0.0"||e.endsWith(".local")}function U(){try{return localStorage.getItem("plugeen_opt_out")==="true"}catch{return!1}}function F(){if(typeof navigator>"u")return!1;let e=navigator.userAgent||"";return!!(navigator.webdriver||/HeadlessChrome/i.test(e)||/PhantomJS/i.test(e)||typeof window<"u"&&window._phantom)}function L(){if(typeof document>"u")return null;let e=document.currentScript,t="";if(!e){let l=document.getElementsByTagName("script");for(let p of Array.from(l))if(p.src&&(p.src.includes("/plugeen.global")||p.src.includes("/plugeen."))){e=p;break}}if(!e)return null;let n={},r=e.getAttribute("data-api-key")??e.getAttribute("data-client-id");r&&(t=r);let i=e.getAttribute("data-api-url");i&&(n.apiUrl=i);let o=e.getAttribute("data-plugins");o&&(n.plugins=o.split(",").map(l=>l.trim()).filter(Boolean));let s=e.getAttribute("data-debug");s!==null&&(n.debug=s==="true"||s==="");let c=e.getAttribute("data-disabled");c!==null&&(n.disabled=c==="true");let d=e.getAttribute("data-sampling-rate");return d!==null&&(n.samplingRate=Number(d)),{apiKey:t,...n}}function y(){if(typeof crypto<"u"&&typeof crypto.randomUUID=="function")return crypto.randomUUID();if(typeof crypto<"u"&&typeof crypto.getRandomValues=="function"){let e=new Uint8Array(16);crypto.getRandomValues(e),e[6]=e[6]&15|64,e[8]=e[8]&63|128;let t=Array.from(e).map(n=>n.toString(16).padStart(2,"0")).join("");return`${t.slice(0,8)}-${t.slice(8,12)}-${t.slice(12,16)}-${t.slice(16,20)}-${t.slice(20)}`}return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,e=>{let t=Math.floor(Math.random()*16);return(e==="x"?t:t&3|8).toString(16)})}var I="plugeen_anon_id",k="plugeen_session_id",h="plugeen_session_ts";var z=18e5;function A(){try{let e=localStorage.getItem(I);return e||(e=`anon_${y()}`,localStorage.setItem(I,e)),e}catch{return`anon_${y()}`}}function H(e){try{localStorage.setItem(I,e)}catch{}}function w(){try{let e=sessionStorage.getItem(k),t=sessionStorage.getItem(h);if(e&&t&&Date.now()-parseInt(t,10)<z)return sessionStorage.setItem(h,String(Date.now())),e;sessionStorage.removeItem(k),sessionStorage.removeItem(h);let n=`sess_${y()}`;return sessionStorage.setItem(k,n),sessionStorage.setItem(h,String(Date.now())),n}catch{return`sess_${y()}`}}var x=e=>new Promise(t=>setTimeout(t,e)),S=class{constructor(t,n,r=3){this.apiUrl=t;this.apiKey=n;this.maxRetries=r}headers(t="POST"){let n={Authorization:`Bearer ${this.apiKey}`,"x-identity-id":A(),"x-session-id":w()};return t!=="GET"&&(n["Content-Type"]="application/json"),n}async post(t,n,r=0){let i=`${this.apiUrl}${t}`;try{let o=await fetch(i,{method:"POST",headers:this.headers("POST"),body:JSON.stringify(n),keepalive:!0,credentials:"omit"});if(o.status===401)return null;if((o.status>=500||o.status===429)&&r<this.maxRetries){let s=.85+Math.random()*.3;return await x(500*2**r*s),this.post(t,n,r+1)}if(o.status>=200&&o.status<300)try{return(await o.json())?.data??null}catch{return null}return null}catch(o){if(o instanceof TypeError&&r<this.maxRetries){let s=.85+Math.random()*.3;return await x(500*2**r*s),this.post(t,n,r+1)}return null}}async get(t,n=0){let r=`${this.apiUrl}${t}`;try{let i=await fetch(r,{method:"GET",headers:this.headers("GET"),credentials:"omit"});if(i.status===401||i.status===404)return null;if((i.status>=500||i.status===429)&&n<this.maxRetries){let o=.85+Math.random()*.3;return await x(500*2**n*o),this.get(t,n+1)}if(i.status>=200&&i.status<300)try{return(await i.json())?.data??null}catch{return null}return null}catch(i){if(i instanceof TypeError&&n<this.maxRetries){let o=.85+Math.random()*.3;return await x(500*2**n*o),this.get(t,n+1)}return null}}beacon(t,n){if(typeof navigator>"u"||!navigator.sendBeacon)return!1;try{let r=new Blob([JSON.stringify(n)],{type:"application/json"});return navigator.sendBeacon(`${this.apiUrl}${t}`,r)}catch{return!1}}send(t,n,r=!1){r&&this.beacon(t,n)||this.post(t,n)}};function X(e){if(e.length===0||typeof window>"u")return!1;let t=window.location.pathname;for(let n of e){if(n===t)return!0;let r=n.indexOf("*");if(r!==-1&&t.startsWith(n.slice(0,r)))return!0}return!1}function a(e){return!!(e.disabled||U()||F()||!e.debug&&_()||X(e.skipPatterns)||e.samplingRate<1&&Math.random()>e.samplingRate)}function N(e,t){return{create:async(n,r={})=>a(t)?null:e.post("/api/events",{name:n,data:r,source:"api"})}}function V(e,t){return{get:n=>a(t)?Promise.resolve(null):e.get(`/api/v1/experiments/${encodeURIComponent(n)}`)}}function j(e,t){return{get:n=>a(t)?Promise.resolve(null):e.get(`/api/v1/feature-flags/${encodeURIComponent(n)}`)}}function $(e,t){return{set:async(n,r={})=>{if(a(t))return null;let i=await e.post("/api/identities",{id:n,...r});return H(n),i}}}function D(e,t){return{send:n=>a(t)?Promise.resolve(null):e.post("/api/v1/logs",n)}}function B(e,t){return{submit:n=>a(t)?Promise.resolve(null):e.post("/api/v1/surveys",n)}}function K(e,t){if(typeof window>"u")return;let n=Date.now(),r=0,i=0,o=0,s=window.location.href,c=()=>{let u=window.scrollY,{scrollHeight:f,clientHeight:T}=document.documentElement,R=f-T;if(R<=0)return;let C=Math.min(100,Math.round(u/R*100));C>r&&(r=C)};window.addEventListener("scroll",c,{passive:!0});let d=()=>{i++};for(let u of["mousedown","keydown","touchstart"])window.addEventListener(u,d,{passive:!0});let l=()=>{o++;let u={event:"page_view",url:window.location.href,title:document.title,sessionId:w(),referrer:document.referrer||void 0,screenWidth:window.innerWidth,screenHeight:window.innerHeight};t.post("/api/v1/analytics",u)},p=()=>({url:s,time_on_page:Math.round((Date.now()-n)/1e3),scroll_depth:r,interaction_count:i,page_count:o}),g=(u=!1)=>{let f=p();u?t.send("/api/events",{name:"page_exit",data:f,source:"api"},!0):e.events.create("page_exit",f)},v=()=>{window.location.href!==s&&(g(),r=0,i=0,n=Date.now(),s=window.location.href,l())},M=u=>{let f=history[u].bind(history);history[u]=(...T)=>{f(...T),v()}};M("pushState"),M("replaceState"),window.addEventListener("popstate",v),window.addEventListener("beforeunload",()=>{g(!0)}),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&g(!0)}),l()}var Q=["chrome-extension://","moz-extension://","safari-extension://","edge-extension://"];function O(e){if(!e)return!1;let t=e.toLowerCase();return Q.some(n=>t.includes(n))}function W(e){typeof window>"u"||(window.addEventListener("error",t=>{O(t.filename)||O(t.error?.stack)||t.error===null&&t.message==="Script error."||e.events.create("error",{message:t.message||"Unknown Error",filename:t.filename,lineno:t.lineno,colno:t.colno,stack:t.error?.stack,error_type:t.error?.name||"Error"})}),window.addEventListener("unhandledrejection",t=>{let{reason:n}=t;if(O(n?.stack))return;let r="Unknown Error",i;n instanceof Error?(r=n.message,i=n.stack):typeof n=="string"?r=n:n!==null&&typeof n=="object"&&"message"in n&&(r=String(n.message)),e.events.create("error",{message:r,stack:i,error_type:"UnhandledRejection"})}))}function m(e,t){return e>t[1]?"poor":e>t[0]?"needs-improvement":"good"}function P(){return performance.getEntriesByType("navigation")[0]?.activationStart??0}function Z(){let e=performance.getEntriesByType("navigation")[0];if(e&&e.responseStart>0&&e.responseStart<performance.now())return e}function b(e,t,n){try{if(!PerformanceObserver.supportedEntryTypes.includes(e))return;let r=new PerformanceObserver(i=>{Promise.resolve().then(()=>t(i.getEntries()))});return r.observe({type:e,buffered:!0,...n??{}}),r}catch{return}}function ee(e){let t=new Set;b("paint",n=>{for(let r of n)if(r.name==="first-contentful-paint"&&!t.has("FCP")){t.add("FCP");let i=Math.max(r.startTime-P(),0);e({name:"FCP",value:Math.round(i),rating:m(i,[1800,3e3])})}})}function te(e){let t=!1,n=b("largest-contentful-paint",i=>{if(t)return;let o=i[i.length-1];if(o){let s=Math.max(o.startTime-P(),0);e({name:"LCP",value:Math.round(s),rating:m(s,[2500,4e3])})}});if(!n)return;let r=()=>{if(t)return;t=!0;let i=n.takeRecords();if(i.length>0){let o=i[i.length-1],s=Math.max(o.startTime-P(),0);e({name:"LCP",value:Math.round(s),rating:m(s,[2500,4e3])})}n.disconnect()};for(let i of["keydown","click","visibilitychange"])addEventListener(i,r,{capture:!0,once:!0})}function ne(e){let t=0,n=[],r=0;b("layout-shift",i=>{for(let o of i){let s=o;if(s.hadRecentInput)continue;let c=n[n.length-1],d=n[0];if(n.length>0&&c&&d&&s.startTime-c.startTime<1e3&&s.startTime-d.startTime<5e3?(t+=s.value,n.push(s)):(t=s.value,n=[s]),t>r){r=t;let l=Math.round(r*1e4)/1e4;e({name:"CLS",value:l,rating:m(r,[.1,.25])})}}})}function re(e){let t=Z();if(!t)return;let n=Math.max(t.responseStart-P(),0);e({name:"TTFB",value:Math.round(n),rating:m(n,[800,1800])})}function ie(e){let t=new Map,n=0;b("event",r=>{for(let i of r){let o=i;if(!o.interactionId)continue;let s=t.get(o.interactionId)??0;o.duration>s&&(t.set(o.interactionId,o.duration),o.duration>n&&(n=o.duration,e({name:"INP",value:Math.round(o.duration),rating:m(o.duration,[200,500])})))}},{durationThreshold:40})}function oe(e){if(typeof requestAnimationFrame>"u")return;let t=0,n=2e3,r=performance.now(),i=()=>{t++,performance.now()-r<n?requestAnimationFrame(i):e({name:"FPS",value:Math.round(t/n*1e3)})};document.readyState==="complete"?requestAnimationFrame(i):window.addEventListener("load",()=>requestAnimationFrame(i),{once:!0})}function q(e){typeof window>"u"||typeof PerformanceObserver>"u"||(ee(e),te(e),ne(e),re(e),ie(e),oe(e))}function Y(e){q(t=>{e.events.create("web_vital",{name:t.name,value:t.value,...t.rating!==void 0?{rating:t.rating}:{}})})}function J(e,t,n){for(let r of n)r==="analytics"?K(e,t):r==="web-vitals"?Y(e):r==="errors"&&W(e)}function G(e,t){if(!e||typeof e!="string")throw new TypeError("[plugeen] apiKey is required");if(!t?.apiUrl||typeof t.apiUrl!="string")throw new TypeError("[plugeen] apiUrl is required");let n={apiUrl:t.apiUrl,debug:t.debug??!1,disabled:t.disabled??!1,samplingRate:t.samplingRate??1,plugins:t.plugins??[],skipPatterns:t.skipPatterns??[]},r=new S(n.apiUrl,e),i=N(r,n),o=$(r,n),s=j(r,n),c=D(r,n),d=B(r,n),l=V(r,n),p={track:(g,v)=>{i.create(g,v??{})},events:i,identities:o,featureFlags:s,logs:c,surveys:d,experiments:l};return n.plugins.length>0&&J(p,r,n.plugins),p}var E=L();if(!E?.apiKey||!E?.apiUrl)console.warn("[plugeen] data-api-key and data-api-url attributes are required.");else{let e=G(E.apiKey,E);window.plugeen=e}})();
2
+ "use strict";var plugeen=(()=>{function P(){if(typeof document>"u")return null;let t=document.currentScript,e="";if(!t){let a=document.getElementsByTagName("script");for(let u of Array.from(a))if(u.src&&(u.src.includes("/plugeen.global")||u.src.includes("/plugeen."))){t=u;break}}if(!t)return null;let n={},r=t.getAttribute("data-api-key");r&&(e=r);let i=t.getAttribute("data-api-url");i&&(n.baseUrl=i);let s=t.getAttribute("data-plugins");s&&(n.plugins=s.split(",").map(a=>a.trim()).filter(Boolean));let o=t.getAttribute("data-debug");return o!==null&&(n.debug=o==="true"||o===""),{apiKey:e,...n}}function m(){if(typeof crypto<"u"&&typeof crypto.randomUUID=="function")return crypto.randomUUID();if(typeof crypto<"u"&&typeof crypto.getRandomValues=="function"){let t=new Uint8Array(16);crypto.getRandomValues(t),t[6]=t[6]&15|64,t[8]=t[8]&63|128;let e=Array.from(t).map(n=>n.toString(16).padStart(2,"0")).join("");return`${e.slice(0,8)}-${e.slice(8,12)}-${e.slice(12,16)}-${e.slice(16,20)}-${e.slice(20)}`}return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,t=>{let e=Math.floor(Math.random()*16);return(t==="x"?e:e&3|8).toString(16)})}var d="plugeen_anon_id",g="plugeen_session_id",c="plugeen_session_ts";var I=18e5,l={};function E(){try{let t=localStorage.getItem(d);return t||(t=`anon_${m()}`,localStorage.setItem(d,t)),t}catch{return l[d]||(l[d]=`anon_${m()}`),l[d]}}function T(t){try{localStorage.setItem(d,t)}catch{l[d]=t}}function y(){try{let t=sessionStorage.getItem(g),e=sessionStorage.getItem(c);if(t&&e&&Date.now()-parseInt(e,10)<I)return sessionStorage.setItem(c,String(Date.now())),t;sessionStorage.removeItem(g),sessionStorage.removeItem(c);let n=`sess_${m()}`;return sessionStorage.setItem(g,n),sessionStorage.setItem(c,String(Date.now())),n}catch{let t=l[g],e=l[c];if(t&&e&&Date.now()-parseInt(e,10)<I)return l[c]=String(Date.now()),t;let n=`sess_${m()}`;return l[g]=n,l[c]=String(Date.now()),n}}var k=t=>new Promise(e=>setTimeout(e,t)),h=class{constructor(e,n,r=3){this.baseUrl=e;this.apiKey=n;this.maxRetries=r}headers(e){let n={Authorization:`Bearer ${this.apiKey}`,"x-identity-id":E(),"x-session-id":y()};return e!=="GET"&&(n["Content-Type"]="application/json"),n}async request(e,{attempt:n=0,method:r,body:i}){let s=`${this.baseUrl}${e}`;try{let o=await fetch(s,{method:r,headers:this.headers(r),credentials:"omit",body:i?JSON.stringify(i):void 0});if(o.status===401||o.status===404)return[null,o.status];if((o.status>=500||o.status===429)&&n<this.maxRetries){let a=.85+Math.random()*.3;return await k(500*2**n*a),this.request(e,{body:i,attempt:n+1,method:r})}if(o.status>=200&&o.status<300)try{return[(await o.json()).data,null]}catch{return[null,"Failed to parse json"]}return[null,"Error"]}catch(o){if(o instanceof TypeError&&n<this.maxRetries){let a=.85+Math.random()*.3;return await k(500*2**n*a),this.get(e,n+1)}return[null,o]}}async post(e,n,r=0){return this.request(e,{attempt:r,body:n,method:"POST"})}async get(e,n=0){return this.request(e,{attempt:n,method:"GET"})}beacon(e,n){if(typeof navigator>"u"||!navigator.sendBeacon)return!1;try{let r=new Blob([JSON.stringify(n)],{type:"application/json"});return navigator.sendBeacon(`${this.baseUrl}${e}`,r)}catch{return!1}}};function C(t){return{create:async(e,n={})=>t.post("/events",{name:e,data:n,source:"api"})}}function O(t){return{create:async(e,n={})=>{let r=await t.post("/identities",{id:e,...n});return T(e),r}}}function _(t){if(typeof window>"u")return;let e=Date.now(),n=0,r=window.location.href,i=()=>{n++,t.post("/v1/analytics",{event:"page_view",url:window.location.href,title:document.title,sessionId:y(),referrer:document.referrer||void 0,screenWidth:window.innerWidth,screenHeight:window.innerHeight})},s=(u=!1)=>{let p={event:"page_exit",url:window.location.href,title:document.title,sessionId:y(),referrer:document.referrer||void 0,screenWidth:window.innerWidth,screenHeight:window.innerHeight,metadata:{timeOnPage:Math.round((Date.now()-e)/1e3),pageCount:n}};u?t.beacon("/v1/analytics",p):t.post("/v1/analytics",p)},o=()=>{window.location.href!==r&&(s(),e=Date.now(),r=window.location.href,i())},a=u=>{let p=history[u].bind(history);history[u]=(...F)=>{p(...F),o()}};a("pushState"),a("replaceState"),window.addEventListener("popstate",o),window.addEventListener("beforeunload",()=>s(!0)),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&s(!0)}),i()}var A=["chrome-extension://","moz-extension://","safari-extension://","edge-extension://"];function b(t){if(!t)return!1;let e=t.toLowerCase();return A.some(n=>e.includes(n))}function U(t){typeof window>"u"||(window.addEventListener("error",e=>{b(e.filename)||b(e.error?.stack)||e.error===null&&e.message==="Script error."||t.post("/v1/logs",{message:e.message||"Unknown Error",filename:e.filename,lineno:e.lineno,colno:e.colno,stack:e.error?.stack,error_type:e.error?.name||"Error"})}),window.addEventListener("unhandledrejection",e=>{let{reason:n}=e;if(b(n?.stack))return;let r="Unknown Error",i;n instanceof Error?(r=n.message,i=n.stack):typeof n=="string"?r=n:n!==null&&typeof n=="object"&&"message"in n&&(r=String(n.message)),t.post("/v1/logs",{message:r,stack:i,error_type:"UnhandledRejection"})}))}function f(t,e){return t>e[1]?"poor":t>e[0]?"needs-improvement":"good"}function v(){return performance.getEntriesByType("navigation")[0]?.activationStart??0}function H(){let t=performance.getEntriesByType("navigation")[0];if(t&&t.responseStart>0&&t.responseStart<performance.now())return t}function w(t,e,n){try{if(!PerformanceObserver.supportedEntryTypes.includes(t))return;let r=new PerformanceObserver(i=>{Promise.resolve().then(()=>e(i.getEntries()))});return r.observe({type:t,buffered:!0,...n??{}}),r}catch{return}}function D(t){let e=new Set;w("paint",n=>{for(let r of n)if(r.name==="first-contentful-paint"&&!e.has("FCP")){e.add("FCP");let i=Math.max(r.startTime-v(),0);t({name:"FCP",value:Math.round(i),rating:f(i,[1800,3e3])})}})}function V(t){let e=!1,n=w("largest-contentful-paint",i=>{if(e)return;let s=i[i.length-1];if(s){let o=Math.max(s.startTime-v(),0);t({name:"LCP",value:Math.round(o),rating:f(o,[2500,4e3])})}});if(!n)return;let r=()=>{if(e)return;e=!0;let i=n.takeRecords();if(i.length>0){let s=i[i.length-1],o=Math.max(s.startTime-v(),0);t({name:"LCP",value:Math.round(o),rating:f(o,[2500,4e3])})}n.disconnect()};for(let i of["keydown","click","visibilitychange"])addEventListener(i,r,{capture:!0,once:!0})}function j(t){let e=0,n=[],r=0;w("layout-shift",i=>{for(let s of i){let o=s;if(o.hadRecentInput)continue;let a=n[n.length-1],u=n[0];if(n.length>0&&a&&u&&o.startTime-a.startTime<1e3&&o.startTime-u.startTime<5e3?(e+=o.value,n.push(o)):(e=o.value,n=[o]),e>r){r=e;let p=Math.round(r*1e4)/1e4;t({name:"CLS",value:p,rating:f(r,[.1,.25])})}}})}function $(t){let e=H();if(!e)return;let n=Math.max(e.responseStart-v(),0);t({name:"TTFB",value:Math.round(n),rating:f(n,[800,1800])})}function B(t){let e=new Map,n=0;w("event",r=>{for(let i of r){let s=i;if(!s.interactionId)continue;let o=e.get(s.interactionId)??0;s.duration>o&&(e.set(s.interactionId,s.duration),s.duration>n&&(n=s.duration,t({name:"INP",value:Math.round(s.duration),rating:f(s.duration,[200,500])})))}},{durationThreshold:40})}function K(t){if(typeof requestAnimationFrame>"u")return;let e=0,n=2e3,r=performance.now(),i=()=>{e++,performance.now()-r<n?requestAnimationFrame(i):t({name:"FPS",value:Math.round(e/n*1e3)})};document.readyState==="complete"?requestAnimationFrame(i):window.addEventListener("load",()=>requestAnimationFrame(i),{once:!0})}function M(t){typeof window>"u"||typeof PerformanceObserver>"u"||(D(t),V(t),j(t),$(t),B(t),K(t))}function R(t){M(e=>{t.post("/v1/web-vitals",{name:e.name,value:e.value,...e.rating!==void 0?{rating:e.rating}:{}})})}function L(t,e){let n={analytics:_(t),"web-vitals":R(t),errors:U(t)};e.map(r=>n[r])}var S={baseUrl:"https://dev.plugeen.app/api",debug:!1,plugins:[]};function N(t,e){let n={baseUrl:e.baseUrl||S.baseUrl,debug:e.debug||S.debug,plugins:e.plugins||S.plugins};if(!t)throw new TypeError("[plugeen] apiKey is required");if(!n.baseUrl)throw new TypeError("[plugeen] baseUrl is required");let r=new h(n.baseUrl,t),i=C(r),s=O(r),o={track:(a,u)=>{i.create(a,u)},identify:(a,u)=>s.create(a,u)};return n.plugins.length>0&&L(r,n.plugins),o}var x=P();if(!x?.apiKey||!x?.baseUrl)console.warn("[plugeen] data-api-key and data-api-url attributes are required.");else{let t=N(x.apiKey,x);window.plugeen=t}})();
package/package.json CHANGED
@@ -1,14 +1,15 @@
1
1
  {
2
2
  "name": "plugeen",
3
- "version": "0.0.8",
4
- "description": "Lightweight plugin-based browser analytics SDK",
5
- "type": "module",
3
+ "version": "0.0.9",
6
4
  "main": "./dist/index.cjs",
7
5
  "module": "./dist/index.js",
8
- "browser": "./dist/index.js",
9
- "unpkg": "./dist/plugeen.global.js",
10
- "jsdelivr": "./dist/plugeen.global.js",
11
- "types": "./dist/index.d.ts",
6
+ "devDependencies": {
7
+ "@biomejs/biome": "2.4.14",
8
+ "@types/node": "25.6.1",
9
+ "concurrently": "9.2.1",
10
+ "tsup": "^8.5.0",
11
+ "typescript": "^5.8.0"
12
+ },
12
13
  "exports": {
13
14
  ".": {
14
15
  "types": "./dist/index.d.ts",
@@ -16,18 +17,19 @@
16
17
  "require": "./dist/index.cjs"
17
18
  }
18
19
  },
20
+ "browser": "./dist/index.js",
21
+ "description": "Lightweight plugin-based browser analytics SDK",
19
22
  "files": [
20
23
  "dist"
21
24
  ],
25
+ "jsdelivr": "./dist/plugeen.global.js",
22
26
  "scripts": {
23
27
  "build": "tsup",
24
- "dev": "tsup --watch",
28
+ "dev": "concurrently 'tsup --watch' 'bun --watch ./src/demo/index.ts'",
25
29
  "biome": "biome check",
26
30
  "typecheck": "tsc --project tsconfig.json --noEmit"
27
31
  },
28
- "devDependencies": {
29
- "@biomejs/biome": "2.4.14",
30
- "tsup": "^8.5.0",
31
- "typescript": "^5.8.0"
32
- }
33
- }
32
+ "type": "module",
33
+ "types": "./dist/index.d.ts",
34
+ "unpkg": "./dist/plugeen.global.js"
35
+ }
package/dist/index.d.cts DELETED
@@ -1,89 +0,0 @@
1
- type PluginName = "analytics" | "web-vitals" | "errors";
2
- interface SdkOptions {
3
- apiUrl: string;
4
- plugins?: PluginName[];
5
- debug?: boolean;
6
- disabled?: boolean;
7
- samplingRate?: number;
8
- skipPatterns?: string[];
9
- }
10
- type EventProperties = Record<string, string | number | boolean | null | undefined | Record<string, unknown>>;
11
- interface Identity {
12
- id: string;
13
- name?: string;
14
- email?: string;
15
- metadata?: Record<string, unknown>;
16
- }
17
- interface LogPayload {
18
- level: "debug" | "info" | "warn" | "error" | "fatal";
19
- message: string;
20
- traceId?: string;
21
- spanId?: string;
22
- service?: string;
23
- environment?: string;
24
- route?: string;
25
- release?: string;
26
- runtime?: string;
27
- file?: string;
28
- line?: number;
29
- stack?: string;
30
- }
31
- interface SurveyPayload {
32
- surveyId: string;
33
- question: string;
34
- response: string;
35
- rating?: number;
36
- sentiment?: "positive" | "neutral" | "negative";
37
- path?: string;
38
- }
39
- interface FeatureFlag {
40
- key: string;
41
- enabled: boolean;
42
- value?: string;
43
- description?: string;
44
- }
45
- interface Experiment {
46
- experimentKey: string;
47
- variant: string;
48
- userId?: string;
49
- metric?: string;
50
- converted?: boolean;
51
- value?: number;
52
- }
53
- type TrackFn = (event: string, properties?: EventProperties) => void;
54
- interface EventsModule {
55
- create<T = unknown>(name: string, data?: EventProperties): Promise<T | null>;
56
- }
57
- interface IdentitiesModule {
58
- set(distinctId: string, data?: Omit<Identity, "id">): Promise<Identity | null>;
59
- }
60
- interface FeatureFlagsModule {
61
- get(key: string): Promise<FeatureFlag | null>;
62
- }
63
- interface LogsModule {
64
- send<T = unknown>(payload: LogPayload): Promise<T | null>;
65
- }
66
- interface SurveysModule {
67
- submit<T = unknown>(payload: SurveyPayload): Promise<T | null>;
68
- }
69
- interface ExperimentsModule {
70
- get(id: string): Promise<Experiment | null>;
71
- }
72
- interface Plugeen {
73
- track: TrackFn;
74
- events: EventsModule;
75
- identities: IdentitiesModule;
76
- featureFlags: FeatureFlagsModule;
77
- logs: LogsModule;
78
- surveys: SurveysModule;
79
- experiments: ExperimentsModule;
80
- }
81
- declare global {
82
- interface Window {
83
- plugeen?: Plugeen;
84
- }
85
- }
86
-
87
- declare function createPlugeen(apiKey: string, options: SdkOptions): Plugeen;
88
-
89
- export { type EventProperties, type Experiment, type FeatureFlag, type Identity, type LogPayload, type Plugeen, type PluginName, type SdkOptions, type SurveyPayload, type TrackFn, createPlugeen };
package/dist/index.d.ts DELETED
@@ -1,89 +0,0 @@
1
- type PluginName = "analytics" | "web-vitals" | "errors";
2
- interface SdkOptions {
3
- apiUrl: string;
4
- plugins?: PluginName[];
5
- debug?: boolean;
6
- disabled?: boolean;
7
- samplingRate?: number;
8
- skipPatterns?: string[];
9
- }
10
- type EventProperties = Record<string, string | number | boolean | null | undefined | Record<string, unknown>>;
11
- interface Identity {
12
- id: string;
13
- name?: string;
14
- email?: string;
15
- metadata?: Record<string, unknown>;
16
- }
17
- interface LogPayload {
18
- level: "debug" | "info" | "warn" | "error" | "fatal";
19
- message: string;
20
- traceId?: string;
21
- spanId?: string;
22
- service?: string;
23
- environment?: string;
24
- route?: string;
25
- release?: string;
26
- runtime?: string;
27
- file?: string;
28
- line?: number;
29
- stack?: string;
30
- }
31
- interface SurveyPayload {
32
- surveyId: string;
33
- question: string;
34
- response: string;
35
- rating?: number;
36
- sentiment?: "positive" | "neutral" | "negative";
37
- path?: string;
38
- }
39
- interface FeatureFlag {
40
- key: string;
41
- enabled: boolean;
42
- value?: string;
43
- description?: string;
44
- }
45
- interface Experiment {
46
- experimentKey: string;
47
- variant: string;
48
- userId?: string;
49
- metric?: string;
50
- converted?: boolean;
51
- value?: number;
52
- }
53
- type TrackFn = (event: string, properties?: EventProperties) => void;
54
- interface EventsModule {
55
- create<T = unknown>(name: string, data?: EventProperties): Promise<T | null>;
56
- }
57
- interface IdentitiesModule {
58
- set(distinctId: string, data?: Omit<Identity, "id">): Promise<Identity | null>;
59
- }
60
- interface FeatureFlagsModule {
61
- get(key: string): Promise<FeatureFlag | null>;
62
- }
63
- interface LogsModule {
64
- send<T = unknown>(payload: LogPayload): Promise<T | null>;
65
- }
66
- interface SurveysModule {
67
- submit<T = unknown>(payload: SurveyPayload): Promise<T | null>;
68
- }
69
- interface ExperimentsModule {
70
- get(id: string): Promise<Experiment | null>;
71
- }
72
- interface Plugeen {
73
- track: TrackFn;
74
- events: EventsModule;
75
- identities: IdentitiesModule;
76
- featureFlags: FeatureFlagsModule;
77
- logs: LogsModule;
78
- surveys: SurveysModule;
79
- experiments: ExperimentsModule;
80
- }
81
- declare global {
82
- interface Window {
83
- plugeen?: Plugeen;
84
- }
85
- }
86
-
87
- declare function createPlugeen(apiKey: string, options: SdkOptions): Plugeen;
88
-
89
- export { type EventProperties, type Experiment, type FeatureFlag, type Identity, type LogPayload, type Plugeen, type PluginName, type SdkOptions, type SurveyPayload, type TrackFn, createPlugeen };