featurely-site-manager 1.1.10 → 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,21 +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
- try {
400
- const res = await fetch(url, {
401
- method: "POST",
402
- headers: { "Content-Type": "application/json" },
403
- body: payload,
404
- keepalive: true
405
- });
406
- if (!res.ok) {
407
- const body = await res.text().catch(() => "(unreadable)");
408
- console.error(`[Featurely] Analytics event "${event.eventName}" rejected by server: ${res.status} ${res.statusText} \u2014 ${body}`);
400
+ if (useBeacon) {
401
+ if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
402
+ navigator.sendBeacon(url, payload);
409
403
  }
410
- } catch (fetchError) {
411
- const sentViaBeacon = typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && navigator.sendBeacon(url, payload);
412
- if (!sentViaBeacon) {
413
- console.error(`[Featurely] Failed to send analytics event "${event.eventName}":`, fetchError);
404
+ } else {
405
+ try {
406
+ const res = await fetch(url, {
407
+ method: "POST",
408
+ headers: { "Content-Type": "application/json" },
409
+ body: payload,
410
+ keepalive: true
411
+ });
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
+ }
414
421
  }
415
422
  }
416
423
  }
@@ -426,8 +433,9 @@ var _SiteManager = class _SiteManager {
426
433
  isLoggedIn: !!this.config.userId
427
434
  });
428
435
  this.currentPagePath = window.location.pathname;
436
+ this.currentPageSearch = window.location.search;
429
437
  this.pageEntryTime = Date.now();
430
- this.trackPageView();
438
+ this.trackPageView(this.currentPagePath);
431
439
  setTimeout(() => this.flushAnalytics(), 2e3);
432
440
  const originalPushState = history.pushState.bind(history);
433
441
  const originalReplaceState = history.replaceState.bind(history);
@@ -438,29 +446,35 @@ var _SiteManager = class _SiteManager {
438
446
  };
439
447
  history.replaceState = function(...args) {
440
448
  originalReplaceState(...args);
441
- 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) {
442
452
  self.onNavigate();
443
453
  }
444
454
  };
445
455
  window.addEventListener("popstate", () => this.onNavigate());
456
+ window.addEventListener("hashchange", () => {
457
+ if (window.location.pathname !== this.currentPagePath) {
458
+ this.onNavigate();
459
+ }
460
+ });
446
461
  window.addEventListener("visibilitychange", () => {
447
462
  if (document.visibilityState === "hidden") {
448
- const duration = Math.round((Date.now() - this.pageEntryTime) / 1e3);
449
- if (duration > 0) {
450
- this.trackEvent("page_exit", {
451
- path: this.currentPagePath,
452
- durationSeconds: duration
453
- });
454
- this.flushAnalytics();
455
- }
463
+ this.trackPageExit();
464
+ this.flushAnalytics(true);
456
465
  } else if (document.visibilityState === "visible") {
457
466
  this.pageEntryTime = Date.now();
458
467
  }
459
468
  });
469
+ window.addEventListener("pagehide", () => {
470
+ this.trackPageExit();
471
+ this.flushAnalytics(true);
472
+ });
460
473
  }
461
- trackPageView() {
474
+ /** Track a page_view for a captured path (path passed in to avoid closure bugs on rapid nav). */
475
+ trackPageView(path) {
462
476
  const props = {
463
- path: this.currentPagePath,
477
+ path,
464
478
  title: document.title,
465
479
  referrer: document.referrer || "",
466
480
  isLoggedIn: !!this.config.userId
@@ -469,23 +483,29 @@ var _SiteManager = class _SiteManager {
469
483
  if (this.config.userEmail) props.userEmail = this.config.userEmail;
470
484
  this.trackEvent("page_view", props);
471
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
+ }
472
497
  onNavigate() {
473
498
  const newPath = window.location.pathname;
474
- if (this.currentPagePath && this.pageEntryTime > 0) {
475
- const duration = Math.round((Date.now() - this.pageEntryTime) / 1e3);
476
- if (duration > 0) {
477
- this.trackEvent("page_exit", {
478
- path: this.currentPagePath,
479
- durationSeconds: duration
480
- });
481
- }
482
- }
499
+ const newSearch = window.location.search;
500
+ this.trackPageExit();
483
501
  this.currentPagePath = newPath;
502
+ this.currentPageSearch = newSearch;
484
503
  this.pageEntryTime = Date.now();
504
+ const capturedPath = newPath;
485
505
  setTimeout(() => {
486
- this.trackPageView();
506
+ this.trackPageView(capturedPath);
487
507
  this.flushAnalytics();
488
- }, 100);
508
+ }, 150);
489
509
  }
490
510
  generateSessionId() {
491
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,21 +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
- try {
365
- const res = await fetch(url, {
366
- method: "POST",
367
- headers: { "Content-Type": "application/json" },
368
- body: payload,
369
- keepalive: true
370
- });
371
- if (!res.ok) {
372
- const body = await res.text().catch(() => "(unreadable)");
373
- console.error(`[Featurely] Analytics event "${event.eventName}" rejected by server: ${res.status} ${res.statusText} \u2014 ${body}`);
365
+ if (useBeacon) {
366
+ if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
367
+ navigator.sendBeacon(url, payload);
374
368
  }
375
- } catch (fetchError) {
376
- const sentViaBeacon = typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && navigator.sendBeacon(url, payload);
377
- if (!sentViaBeacon) {
378
- console.error(`[Featurely] Failed to send analytics event "${event.eventName}":`, fetchError);
369
+ } else {
370
+ try {
371
+ const res = await fetch(url, {
372
+ method: "POST",
373
+ headers: { "Content-Type": "application/json" },
374
+ body: payload,
375
+ keepalive: true
376
+ });
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
+ }
379
386
  }
380
387
  }
381
388
  }
@@ -391,8 +398,9 @@ var _SiteManager = class _SiteManager {
391
398
  isLoggedIn: !!this.config.userId
392
399
  });
393
400
  this.currentPagePath = window.location.pathname;
401
+ this.currentPageSearch = window.location.search;
394
402
  this.pageEntryTime = Date.now();
395
- this.trackPageView();
403
+ this.trackPageView(this.currentPagePath);
396
404
  setTimeout(() => this.flushAnalytics(), 2e3);
397
405
  const originalPushState = history.pushState.bind(history);
398
406
  const originalReplaceState = history.replaceState.bind(history);
@@ -403,29 +411,35 @@ var _SiteManager = class _SiteManager {
403
411
  };
404
412
  history.replaceState = function(...args) {
405
413
  originalReplaceState(...args);
406
- 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) {
407
417
  self.onNavigate();
408
418
  }
409
419
  };
410
420
  window.addEventListener("popstate", () => this.onNavigate());
421
+ window.addEventListener("hashchange", () => {
422
+ if (window.location.pathname !== this.currentPagePath) {
423
+ this.onNavigate();
424
+ }
425
+ });
411
426
  window.addEventListener("visibilitychange", () => {
412
427
  if (document.visibilityState === "hidden") {
413
- const duration = Math.round((Date.now() - this.pageEntryTime) / 1e3);
414
- if (duration > 0) {
415
- this.trackEvent("page_exit", {
416
- path: this.currentPagePath,
417
- durationSeconds: duration
418
- });
419
- this.flushAnalytics();
420
- }
428
+ this.trackPageExit();
429
+ this.flushAnalytics(true);
421
430
  } else if (document.visibilityState === "visible") {
422
431
  this.pageEntryTime = Date.now();
423
432
  }
424
433
  });
434
+ window.addEventListener("pagehide", () => {
435
+ this.trackPageExit();
436
+ this.flushAnalytics(true);
437
+ });
425
438
  }
426
- trackPageView() {
439
+ /** Track a page_view for a captured path (path passed in to avoid closure bugs on rapid nav). */
440
+ trackPageView(path) {
427
441
  const props = {
428
- path: this.currentPagePath,
442
+ path,
429
443
  title: document.title,
430
444
  referrer: document.referrer || "",
431
445
  isLoggedIn: !!this.config.userId
@@ -434,23 +448,29 @@ var _SiteManager = class _SiteManager {
434
448
  if (this.config.userEmail) props.userEmail = this.config.userEmail;
435
449
  this.trackEvent("page_view", props);
436
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
+ }
437
462
  onNavigate() {
438
463
  const newPath = window.location.pathname;
439
- if (this.currentPagePath && this.pageEntryTime > 0) {
440
- const duration = Math.round((Date.now() - this.pageEntryTime) / 1e3);
441
- if (duration > 0) {
442
- this.trackEvent("page_exit", {
443
- path: this.currentPagePath,
444
- durationSeconds: duration
445
- });
446
- }
447
- }
464
+ const newSearch = window.location.search;
465
+ this.trackPageExit();
448
466
  this.currentPagePath = newPath;
467
+ this.currentPageSearch = newSearch;
449
468
  this.pageEntryTime = Date.now();
469
+ const capturedPath = newPath;
450
470
  setTimeout(() => {
451
- this.trackPageView();
471
+ this.trackPageView(capturedPath);
452
472
  this.flushAnalytics();
453
- }, 100);
473
+ }, 150);
454
474
  }
455
475
  generateSessionId() {
456
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.10",
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",