featurely-site-manager 1.1.9 → 1.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -105,6 +105,7 @@ declare class SiteManager {
105
105
  private consecutiveFetchFailures;
106
106
  private pageTrackingSetup;
107
107
  private currentPagePath;
108
+ private currentPageSearch;
108
109
  private pageEntryTime;
109
110
  private static readonly MAX_CONSECUTIVE_FAILURES;
110
111
  private lastVersionCheck;
@@ -128,6 +129,7 @@ declare class SiteManager {
128
129
  private flushAnalytics;
129
130
  private setupPageTracking;
130
131
  private trackPageView;
132
+ private trackPageExit;
131
133
  private onNavigate;
132
134
  private generateSessionId;
133
135
  checkVersion(currentVersion?: string): Promise<VersionCheckResponse | null>;
package/dist/index.d.ts CHANGED
@@ -105,6 +105,7 @@ declare class SiteManager {
105
105
  private consecutiveFetchFailures;
106
106
  private pageTrackingSetup;
107
107
  private currentPagePath;
108
+ private currentPageSearch;
108
109
  private pageEntryTime;
109
110
  private static readonly MAX_CONSECUTIVE_FAILURES;
110
111
  private lastVersionCheck;
@@ -128,6 +129,7 @@ declare class SiteManager {
128
129
  private flushAnalytics;
129
130
  private setupPageTracking;
130
131
  private trackPageView;
132
+ private trackPageExit;
131
133
  private onNavigate;
132
134
  private generateSessionId;
133
135
  checkVersion(currentVersion?: string): Promise<VersionCheckResponse | null>;
package/dist/index.js CHANGED
@@ -50,6 +50,7 @@ var _SiteManager = class _SiteManager {
50
50
  this.consecutiveFetchFailures = 0;
51
51
  this.pageTrackingSetup = false;
52
52
  this.currentPagePath = "";
53
+ this.currentPageSearch = "";
53
54
  this.pageEntryTime = 0;
54
55
  this.lastVersionCheck = null;
55
56
  var _a, _b, _c, _d, _e;
@@ -379,7 +380,7 @@ var _SiteManager = class _SiteManager {
379
380
  this.analyticsFlushIntervalId = null;
380
381
  }
381
382
  }
382
- async flushAnalytics() {
383
+ async flushAnalytics(useBeacon = false) {
383
384
  if (this.analyticsQueue.length === 0) {
384
385
  return;
385
386
  }
@@ -396,16 +397,27 @@ var _SiteManager = class _SiteManager {
396
397
  platform: typeof navigator !== "undefined" ? navigator.platform : void 0,
397
398
  appVersion: this.config.appVersion
398
399
  });
399
- const sentViaBeacon = typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && navigator.sendBeacon(url, payload);
400
- if (!sentViaBeacon) {
400
+ if (useBeacon) {
401
+ if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
402
+ navigator.sendBeacon(url, payload);
403
+ }
404
+ } else {
401
405
  try {
402
- await fetch(url, {
406
+ const res = await fetch(url, {
403
407
  method: "POST",
404
408
  headers: { "Content-Type": "application/json" },
405
- body: payload
409
+ body: payload,
410
+ keepalive: true
406
411
  });
407
- } catch (error) {
408
- console.error("Failed to send analytics event:", error);
412
+ if (!res.ok) {
413
+ const body = await res.text().catch(() => "(unreadable)");
414
+ console.error(`[Featurely] Analytics event "${event.eventName}" rejected: ${res.status} ${res.statusText} \u2014 ${body}`);
415
+ }
416
+ } catch (fetchError) {
417
+ const sentViaBeacon = typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && navigator.sendBeacon(url, payload);
418
+ if (!sentViaBeacon) {
419
+ console.error(`[Featurely] Failed to send analytics event "${event.eventName}":`, fetchError);
420
+ }
409
421
  }
410
422
  }
411
423
  }
@@ -421,8 +433,9 @@ var _SiteManager = class _SiteManager {
421
433
  isLoggedIn: !!this.config.userId
422
434
  });
423
435
  this.currentPagePath = window.location.pathname;
436
+ this.currentPageSearch = window.location.search;
424
437
  this.pageEntryTime = Date.now();
425
- this.trackPageView();
438
+ this.trackPageView(this.currentPagePath);
426
439
  setTimeout(() => this.flushAnalytics(), 2e3);
427
440
  const originalPushState = history.pushState.bind(history);
428
441
  const originalReplaceState = history.replaceState.bind(history);
@@ -433,29 +446,35 @@ var _SiteManager = class _SiteManager {
433
446
  };
434
447
  history.replaceState = function(...args) {
435
448
  originalReplaceState(...args);
436
- if (window.location.pathname !== self.currentPagePath) {
449
+ const nextFull = window.location.pathname + window.location.search;
450
+ const prevFull = self.currentPagePath + self.currentPageSearch;
451
+ if (nextFull !== prevFull) {
437
452
  self.onNavigate();
438
453
  }
439
454
  };
440
455
  window.addEventListener("popstate", () => this.onNavigate());
456
+ window.addEventListener("hashchange", () => {
457
+ if (window.location.pathname !== this.currentPagePath) {
458
+ this.onNavigate();
459
+ }
460
+ });
441
461
  window.addEventListener("visibilitychange", () => {
442
462
  if (document.visibilityState === "hidden") {
443
- const duration = Math.round((Date.now() - this.pageEntryTime) / 1e3);
444
- if (duration > 0) {
445
- this.trackEvent("page_exit", {
446
- path: this.currentPagePath,
447
- durationSeconds: duration
448
- });
449
- this.flushAnalytics();
450
- }
463
+ this.trackPageExit();
464
+ this.flushAnalytics(true);
451
465
  } else if (document.visibilityState === "visible") {
452
466
  this.pageEntryTime = Date.now();
453
467
  }
454
468
  });
469
+ window.addEventListener("pagehide", () => {
470
+ this.trackPageExit();
471
+ this.flushAnalytics(true);
472
+ });
455
473
  }
456
- trackPageView() {
474
+ /** Track a page_view for a captured path (path passed in to avoid closure bugs on rapid nav). */
475
+ trackPageView(path) {
457
476
  const props = {
458
- path: this.currentPagePath,
477
+ path,
459
478
  title: document.title,
460
479
  referrer: document.referrer || "",
461
480
  isLoggedIn: !!this.config.userId
@@ -464,23 +483,29 @@ var _SiteManager = class _SiteManager {
464
483
  if (this.config.userEmail) props.userEmail = this.config.userEmail;
465
484
  this.trackEvent("page_view", props);
466
485
  }
486
+ /** Record how long the user spent on the current page (called before navigation or unload). */
487
+ trackPageExit() {
488
+ if (!this.currentPagePath || this.pageEntryTime <= 0) return;
489
+ const duration = Math.round((Date.now() - this.pageEntryTime) / 1e3);
490
+ this.trackEvent("page_exit", {
491
+ path: this.currentPagePath,
492
+ durationSeconds: duration
493
+ // 0 is valid — records bounces/instant navigation
494
+ });
495
+ this.pageEntryTime = 0;
496
+ }
467
497
  onNavigate() {
468
498
  const newPath = window.location.pathname;
469
- if (this.currentPagePath && this.pageEntryTime > 0) {
470
- const duration = Math.round((Date.now() - this.pageEntryTime) / 1e3);
471
- if (duration > 0) {
472
- this.trackEvent("page_exit", {
473
- path: this.currentPagePath,
474
- durationSeconds: duration
475
- });
476
- }
477
- }
499
+ const newSearch = window.location.search;
500
+ this.trackPageExit();
478
501
  this.currentPagePath = newPath;
502
+ this.currentPageSearch = newSearch;
479
503
  this.pageEntryTime = Date.now();
504
+ const capturedPath = newPath;
480
505
  setTimeout(() => {
481
- this.trackPageView();
506
+ this.trackPageView(capturedPath);
482
507
  this.flushAnalytics();
483
- }, 100);
508
+ }, 150);
484
509
  }
485
510
  generateSessionId() {
486
511
  if (typeof crypto !== "undefined" && crypto.getRandomValues) {
package/dist/index.mjs CHANGED
@@ -15,6 +15,7 @@ var _SiteManager = class _SiteManager {
15
15
  this.consecutiveFetchFailures = 0;
16
16
  this.pageTrackingSetup = false;
17
17
  this.currentPagePath = "";
18
+ this.currentPageSearch = "";
18
19
  this.pageEntryTime = 0;
19
20
  this.lastVersionCheck = null;
20
21
  var _a, _b, _c, _d, _e;
@@ -344,7 +345,7 @@ var _SiteManager = class _SiteManager {
344
345
  this.analyticsFlushIntervalId = null;
345
346
  }
346
347
  }
347
- async flushAnalytics() {
348
+ async flushAnalytics(useBeacon = false) {
348
349
  if (this.analyticsQueue.length === 0) {
349
350
  return;
350
351
  }
@@ -361,16 +362,27 @@ var _SiteManager = class _SiteManager {
361
362
  platform: typeof navigator !== "undefined" ? navigator.platform : void 0,
362
363
  appVersion: this.config.appVersion
363
364
  });
364
- const sentViaBeacon = typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && navigator.sendBeacon(url, payload);
365
- if (!sentViaBeacon) {
365
+ if (useBeacon) {
366
+ if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
367
+ navigator.sendBeacon(url, payload);
368
+ }
369
+ } else {
366
370
  try {
367
- await fetch(url, {
371
+ const res = await fetch(url, {
368
372
  method: "POST",
369
373
  headers: { "Content-Type": "application/json" },
370
- body: payload
374
+ body: payload,
375
+ keepalive: true
371
376
  });
372
- } catch (error) {
373
- console.error("Failed to send analytics event:", error);
377
+ if (!res.ok) {
378
+ const body = await res.text().catch(() => "(unreadable)");
379
+ console.error(`[Featurely] Analytics event "${event.eventName}" rejected: ${res.status} ${res.statusText} \u2014 ${body}`);
380
+ }
381
+ } catch (fetchError) {
382
+ const sentViaBeacon = typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && navigator.sendBeacon(url, payload);
383
+ if (!sentViaBeacon) {
384
+ console.error(`[Featurely] Failed to send analytics event "${event.eventName}":`, fetchError);
385
+ }
374
386
  }
375
387
  }
376
388
  }
@@ -386,8 +398,9 @@ var _SiteManager = class _SiteManager {
386
398
  isLoggedIn: !!this.config.userId
387
399
  });
388
400
  this.currentPagePath = window.location.pathname;
401
+ this.currentPageSearch = window.location.search;
389
402
  this.pageEntryTime = Date.now();
390
- this.trackPageView();
403
+ this.trackPageView(this.currentPagePath);
391
404
  setTimeout(() => this.flushAnalytics(), 2e3);
392
405
  const originalPushState = history.pushState.bind(history);
393
406
  const originalReplaceState = history.replaceState.bind(history);
@@ -398,29 +411,35 @@ var _SiteManager = class _SiteManager {
398
411
  };
399
412
  history.replaceState = function(...args) {
400
413
  originalReplaceState(...args);
401
- if (window.location.pathname !== self.currentPagePath) {
414
+ const nextFull = window.location.pathname + window.location.search;
415
+ const prevFull = self.currentPagePath + self.currentPageSearch;
416
+ if (nextFull !== prevFull) {
402
417
  self.onNavigate();
403
418
  }
404
419
  };
405
420
  window.addEventListener("popstate", () => this.onNavigate());
421
+ window.addEventListener("hashchange", () => {
422
+ if (window.location.pathname !== this.currentPagePath) {
423
+ this.onNavigate();
424
+ }
425
+ });
406
426
  window.addEventListener("visibilitychange", () => {
407
427
  if (document.visibilityState === "hidden") {
408
- const duration = Math.round((Date.now() - this.pageEntryTime) / 1e3);
409
- if (duration > 0) {
410
- this.trackEvent("page_exit", {
411
- path: this.currentPagePath,
412
- durationSeconds: duration
413
- });
414
- this.flushAnalytics();
415
- }
428
+ this.trackPageExit();
429
+ this.flushAnalytics(true);
416
430
  } else if (document.visibilityState === "visible") {
417
431
  this.pageEntryTime = Date.now();
418
432
  }
419
433
  });
434
+ window.addEventListener("pagehide", () => {
435
+ this.trackPageExit();
436
+ this.flushAnalytics(true);
437
+ });
420
438
  }
421
- trackPageView() {
439
+ /** Track a page_view for a captured path (path passed in to avoid closure bugs on rapid nav). */
440
+ trackPageView(path) {
422
441
  const props = {
423
- path: this.currentPagePath,
442
+ path,
424
443
  title: document.title,
425
444
  referrer: document.referrer || "",
426
445
  isLoggedIn: !!this.config.userId
@@ -429,23 +448,29 @@ var _SiteManager = class _SiteManager {
429
448
  if (this.config.userEmail) props.userEmail = this.config.userEmail;
430
449
  this.trackEvent("page_view", props);
431
450
  }
451
+ /** Record how long the user spent on the current page (called before navigation or unload). */
452
+ trackPageExit() {
453
+ if (!this.currentPagePath || this.pageEntryTime <= 0) return;
454
+ const duration = Math.round((Date.now() - this.pageEntryTime) / 1e3);
455
+ this.trackEvent("page_exit", {
456
+ path: this.currentPagePath,
457
+ durationSeconds: duration
458
+ // 0 is valid — records bounces/instant navigation
459
+ });
460
+ this.pageEntryTime = 0;
461
+ }
432
462
  onNavigate() {
433
463
  const newPath = window.location.pathname;
434
- if (this.currentPagePath && this.pageEntryTime > 0) {
435
- const duration = Math.round((Date.now() - this.pageEntryTime) / 1e3);
436
- if (duration > 0) {
437
- this.trackEvent("page_exit", {
438
- path: this.currentPagePath,
439
- durationSeconds: duration
440
- });
441
- }
442
- }
464
+ const newSearch = window.location.search;
465
+ this.trackPageExit();
443
466
  this.currentPagePath = newPath;
467
+ this.currentPageSearch = newSearch;
444
468
  this.pageEntryTime = Date.now();
469
+ const capturedPath = newPath;
445
470
  setTimeout(() => {
446
- this.trackPageView();
471
+ this.trackPageView(capturedPath);
447
472
  this.flushAnalytics();
448
- }, 100);
473
+ }, 150);
449
474
  }
450
475
  generateSessionId() {
451
476
  if (typeof crypto !== "undefined" && crypto.getRandomValues) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "featurely-site-manager",
3
- "version": "1.1.9",
3
+ "version": "1.1.11",
4
4
  "description": "Complete site management SDK for maintenance mode, status messages, feature flags, version checking, and analytics",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",