featurely-site-manager 1.1.4 → 1.1.6

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/README.md CHANGED
@@ -63,9 +63,11 @@ siteManager.init();
63
63
 
64
64
  ### 📊 Analytics
65
65
 
66
+ - Automatic page view tracking (SPA-compatible)
67
+ - Session start and end tracking
68
+ - User login event tracking
69
+ - Page exit tracking with time-on-page duration
66
70
  - Track custom events
67
- - Automatic feature flag usage tracking
68
- - Session management
69
71
  - User identification
70
72
 
71
73
  ## 📖 Usage
@@ -216,6 +218,7 @@ const manager = new SiteManager({
216
218
  });
217
219
 
218
220
  manager.init();
221
+ // Automatically tracks: session_start, page_view, page_exit on every navigation
219
222
 
220
223
  // Track custom events
221
224
  manager.trackEvent("button_clicked", {
@@ -223,12 +226,25 @@ manager.trackEvent("button_clicked", {
223
226
  page: "/home",
224
227
  });
225
228
 
226
- manager.trackEvent("feature_used", {
227
- feature: "dark_mode",
228
- enabled: true,
229
+ manager.trackEvent("purchase_completed", {
230
+ amount: 99.99,
231
+ plan: "pro",
229
232
  });
230
233
  ```
231
234
 
235
+ #### Automatic Events
236
+
237
+ When `enableAnalytics` is `true` (default), the SDK automatically tracks:
238
+
239
+ | Event | When | Properties |
240
+ | --- | --- | --- |
241
+ | `session_start` | On first page load | `path`, `title`, `referrer` |
242
+ | `page_view` | On every page navigation | `path`, `title`, `referrer`, `isLoggedIn`, `userId?`, `userEmail?` |
243
+ | `page_exit` | When navigating away from a page | `path`, `durationSeconds` |
244
+ | `user_login` | First time `setUser()` is called with a userId | `userId`, `userEmail?`, `userName?` |
245
+
246
+ Page tracking is SPA-compatible — it patches `history.pushState` and `history.replaceState` and listens to `popstate` so navigations in React, Next.js, Vue, and similar frameworks are captured automatically.
247
+
232
248
  ### Custom Poll Interval
233
249
 
234
250
  ```typescript
@@ -280,12 +296,12 @@ Initialize and start the site manager.
280
296
  await manager.init();
281
297
  ```
282
298
 
283
- #### `setUser(email: string, userId?: string)`
299
+ #### `setUser(email: string, userId?: string, userName?: string)`
284
300
 
285
- Update user email and ID for whitelist and targeting.
301
+ Update user context for whitelist, targeting, and analytics. If `userId` is provided for the first time, a `user_login` analytics event is fired automatically.
286
302
 
287
303
  ```typescript
288
- manager.setUser("user@example.com", "user_123");
304
+ manager.setUser("user@example.com", "user_123", "Jane Doe");
289
305
  ```
290
306
 
291
307
  #### `refresh()`
@@ -464,56 +480,29 @@ const manager = new SiteManager({
464
480
  manager.init();
465
481
  ```
466
482
 
467
- ## 🔧 Configuration
483
+ ## 🔒 Content Security Policy (CSP)
468
484
 
469
- ### SiteManagerConfig
470
-
471
- | Option | Type | Required | Default | Description |
472
- | ----------------------- | --------------- | -------- | ------------------------- | ----------------------------- |
473
- | `apiKey` | `string` | ✅ | - | Your Featurely API key |
474
- | `projectId` | `string` | ✅ | - | Your Featurely project ID |
475
- | `apiUrl` | `string` | ❌ | `'https://featurely.no'` | Custom API endpoint |
476
- | `pollInterval` | `number` | ❌ | `60000` | Polling interval in ms |
477
- | `userEmail` | `string` | ❌ | - | User email for whitelist |
478
- | `bypassCheck` | `() => boolean` | ❌ | - | Custom bypass function |
479
- | `onMaintenanceEnabled` | `function` | ❌ | - | Maintenance enabled callback |
480
- | `onMaintenanceDisabled` | `function` | ❌ | - | Maintenance disabled callback |
481
- | `onMessageReceived` | `function` | ❌ | - | Message received callback |
482
- | `onMessageDismissed` | `function` | ❌ | - | Message dismissed callback |
483
- | `onError` | `function` | ❌ | - | Error callback |
484
-
485
- ## 🎯 API Methods
485
+ If your site uses a `Content-Security-Policy` header, you must allow connections to `https://www.featurely.no`. Without this, all API calls (config polling, analytics, feature flags) will be silently blocked.
486
486
 
487
- ### `init()`
487
+ Add the following to your `connect-src` directive:
488
488
 
489
- Initialize and start the site manager.
490
-
491
- ```typescript
492
- await manager.init();
493
489
  ```
494
-
495
- ### `setUser(email: string)`
496
-
497
- Update user email for whitelist checks.
498
-
499
- ```typescript
500
- manager.setUser("user@example.com");
490
+ connect-src 'self' https://www.featurely.no;
501
491
  ```
502
492
 
503
- ### `refresh()`
504
-
505
- Manually refresh configuration from server.
493
+ **Next.js example** (`next.config.js`):
506
494
 
507
- ```typescript
508
- await manager.refresh();
495
+ ```javascript
496
+ const cspHeader = `
497
+ connect-src 'self' https://www.featurely.no;
498
+ `.trim();
509
499
  ```
510
500
 
511
- ### `destroy()`
512
-
513
- Stop the manager and clean up.
501
+ If the SDK is being blocked by CSP, you will see a message like this in the browser console:
514
502
 
515
- ```typescript
516
- manager.destroy();
503
+ ```
504
+ [SiteManager] ⚠️ Connection to Featurely was blocked — check your Content-Security-Policy.
505
+ Add "https://www.featurely.no" to your connect-src directive.
517
506
  ```
518
507
 
519
508
  ## 🎨 Maintenance Mode
package/dist/index.d.mts CHANGED
@@ -103,6 +103,9 @@ declare class SiteManager {
103
103
  private analyticsFlushIntervalId;
104
104
  private sessionId;
105
105
  private consecutiveFetchFailures;
106
+ private pageTrackingSetup;
107
+ private currentPagePath;
108
+ private pageEntryTime;
106
109
  private static readonly MAX_CONSECUTIVE_FAILURES;
107
110
  private lastVersionCheck;
108
111
  constructor(config: SiteManagerConfig);
@@ -123,6 +126,9 @@ declare class SiteManager {
123
126
  private startAnalyticsFlushing;
124
127
  private stopAnalyticsFlushing;
125
128
  private flushAnalytics;
129
+ private setupPageTracking;
130
+ private trackPageView;
131
+ private onNavigate;
126
132
  private generateSessionId;
127
133
  checkVersion(currentVersion?: string): Promise<VersionCheckResponse | null>;
128
134
  getLastVersionCheck(): VersionCheckResponse | null;
package/dist/index.d.ts CHANGED
@@ -103,6 +103,9 @@ declare class SiteManager {
103
103
  private analyticsFlushIntervalId;
104
104
  private sessionId;
105
105
  private consecutiveFetchFailures;
106
+ private pageTrackingSetup;
107
+ private currentPagePath;
108
+ private pageEntryTime;
106
109
  private static readonly MAX_CONSECUTIVE_FAILURES;
107
110
  private lastVersionCheck;
108
111
  constructor(config: SiteManagerConfig);
@@ -123,6 +126,9 @@ declare class SiteManager {
123
126
  private startAnalyticsFlushing;
124
127
  private stopAnalyticsFlushing;
125
128
  private flushAnalytics;
129
+ private setupPageTracking;
130
+ private trackPageView;
131
+ private onNavigate;
126
132
  private generateSessionId;
127
133
  checkVersion(currentVersion?: string): Promise<VersionCheckResponse | null>;
128
134
  getLastVersionCheck(): VersionCheckResponse | null;
package/dist/index.js CHANGED
@@ -48,6 +48,9 @@ var _SiteManager = class _SiteManager {
48
48
  this.analyticsFlushIntervalId = null;
49
49
  this.sessionId = this.generateSessionId();
50
50
  this.consecutiveFetchFailures = 0;
51
+ this.pageTrackingSetup = false;
52
+ this.currentPagePath = "";
53
+ this.pageEntryTime = 0;
51
54
  this.lastVersionCheck = null;
52
55
  var _a, _b, _c, _d, _e;
53
56
  if (!config.apiKey) {
@@ -95,6 +98,7 @@ var _SiteManager = class _SiteManager {
95
98
  this.startPolling();
96
99
  if (this.config.enableAnalytics) {
97
100
  this.startAnalyticsFlushing();
101
+ this.setupPageTracking();
98
102
  }
99
103
  if (this.config.enableVersionCheck && this.config.appVersion) {
100
104
  await this.checkVersion();
@@ -117,10 +121,17 @@ var _SiteManager = class _SiteManager {
117
121
  */
118
122
  setUser(email, userId) {
119
123
  var _a;
124
+ const wasLoggedIn = !!this.config.userId;
120
125
  this.config.userEmail = email;
121
126
  if (userId) {
122
127
  this.config.userId = userId;
123
128
  }
129
+ if (!wasLoggedIn && this.config.userId && this.config.enableAnalytics) {
130
+ this.trackEvent("user_login", {
131
+ userId: this.config.userId,
132
+ ...this.config.userEmail ? { userEmail: this.config.userEmail } : {}
133
+ });
134
+ }
124
135
  if ((_a = this.siteConfig) == null ? void 0 : _a.maintenance.enabled) {
125
136
  this.checkMaintenanceMode();
126
137
  }
@@ -399,6 +410,74 @@ var _SiteManager = class _SiteManager {
399
410
  }
400
411
  }
401
412
  }
413
+ setupPageTracking() {
414
+ if (this.pageTrackingSetup) return;
415
+ this.pageTrackingSetup = true;
416
+ this.trackEvent("session_start", {
417
+ referrer: document.referrer || "",
418
+ language: navigator.language,
419
+ screenWidth: window.screen.width,
420
+ screenHeight: window.screen.height,
421
+ isLoggedIn: !!this.config.userId
422
+ });
423
+ this.currentPagePath = window.location.pathname;
424
+ this.pageEntryTime = Date.now();
425
+ this.trackPageView();
426
+ const originalPushState = history.pushState.bind(history);
427
+ const originalReplaceState = history.replaceState.bind(history);
428
+ const self = this;
429
+ history.pushState = function(...args) {
430
+ originalPushState(...args);
431
+ self.onNavigate();
432
+ };
433
+ history.replaceState = function(...args) {
434
+ originalReplaceState(...args);
435
+ if (window.location.pathname !== self.currentPagePath) {
436
+ self.onNavigate();
437
+ }
438
+ };
439
+ window.addEventListener("popstate", () => this.onNavigate());
440
+ window.addEventListener("visibilitychange", () => {
441
+ if (document.visibilityState === "hidden") {
442
+ const duration = Math.round((Date.now() - this.pageEntryTime) / 1e3);
443
+ if (duration > 0) {
444
+ this.trackEvent("page_exit", {
445
+ path: this.currentPagePath,
446
+ durationSeconds: duration
447
+ });
448
+ this.flushAnalytics();
449
+ }
450
+ } else if (document.visibilityState === "visible") {
451
+ this.pageEntryTime = Date.now();
452
+ }
453
+ });
454
+ }
455
+ trackPageView() {
456
+ const props = {
457
+ path: this.currentPagePath,
458
+ title: document.title,
459
+ referrer: document.referrer || "",
460
+ isLoggedIn: !!this.config.userId
461
+ };
462
+ if (this.config.userId) props.userId = this.config.userId;
463
+ if (this.config.userEmail) props.userEmail = this.config.userEmail;
464
+ this.trackEvent("page_view", props);
465
+ }
466
+ onNavigate() {
467
+ const newPath = window.location.pathname;
468
+ if (this.currentPagePath && this.pageEntryTime > 0) {
469
+ const duration = Math.round((Date.now() - this.pageEntryTime) / 1e3);
470
+ if (duration > 0) {
471
+ this.trackEvent("page_exit", {
472
+ path: this.currentPagePath,
473
+ durationSeconds: duration
474
+ });
475
+ }
476
+ }
477
+ this.currentPagePath = newPath;
478
+ this.pageEntryTime = Date.now();
479
+ setTimeout(() => this.trackPageView(), 100);
480
+ }
402
481
  generateSessionId() {
403
482
  if (typeof crypto !== "undefined" && crypto.getRandomValues) {
404
483
  const array = new Uint8Array(16);
package/dist/index.mjs CHANGED
@@ -13,6 +13,9 @@ var _SiteManager = class _SiteManager {
13
13
  this.analyticsFlushIntervalId = null;
14
14
  this.sessionId = this.generateSessionId();
15
15
  this.consecutiveFetchFailures = 0;
16
+ this.pageTrackingSetup = false;
17
+ this.currentPagePath = "";
18
+ this.pageEntryTime = 0;
16
19
  this.lastVersionCheck = null;
17
20
  var _a, _b, _c, _d, _e;
18
21
  if (!config.apiKey) {
@@ -60,6 +63,7 @@ var _SiteManager = class _SiteManager {
60
63
  this.startPolling();
61
64
  if (this.config.enableAnalytics) {
62
65
  this.startAnalyticsFlushing();
66
+ this.setupPageTracking();
63
67
  }
64
68
  if (this.config.enableVersionCheck && this.config.appVersion) {
65
69
  await this.checkVersion();
@@ -82,10 +86,17 @@ var _SiteManager = class _SiteManager {
82
86
  */
83
87
  setUser(email, userId) {
84
88
  var _a;
89
+ const wasLoggedIn = !!this.config.userId;
85
90
  this.config.userEmail = email;
86
91
  if (userId) {
87
92
  this.config.userId = userId;
88
93
  }
94
+ if (!wasLoggedIn && this.config.userId && this.config.enableAnalytics) {
95
+ this.trackEvent("user_login", {
96
+ userId: this.config.userId,
97
+ ...this.config.userEmail ? { userEmail: this.config.userEmail } : {}
98
+ });
99
+ }
89
100
  if ((_a = this.siteConfig) == null ? void 0 : _a.maintenance.enabled) {
90
101
  this.checkMaintenanceMode();
91
102
  }
@@ -364,6 +375,74 @@ var _SiteManager = class _SiteManager {
364
375
  }
365
376
  }
366
377
  }
378
+ setupPageTracking() {
379
+ if (this.pageTrackingSetup) return;
380
+ this.pageTrackingSetup = true;
381
+ this.trackEvent("session_start", {
382
+ referrer: document.referrer || "",
383
+ language: navigator.language,
384
+ screenWidth: window.screen.width,
385
+ screenHeight: window.screen.height,
386
+ isLoggedIn: !!this.config.userId
387
+ });
388
+ this.currentPagePath = window.location.pathname;
389
+ this.pageEntryTime = Date.now();
390
+ this.trackPageView();
391
+ const originalPushState = history.pushState.bind(history);
392
+ const originalReplaceState = history.replaceState.bind(history);
393
+ const self = this;
394
+ history.pushState = function(...args) {
395
+ originalPushState(...args);
396
+ self.onNavigate();
397
+ };
398
+ history.replaceState = function(...args) {
399
+ originalReplaceState(...args);
400
+ if (window.location.pathname !== self.currentPagePath) {
401
+ self.onNavigate();
402
+ }
403
+ };
404
+ window.addEventListener("popstate", () => this.onNavigate());
405
+ window.addEventListener("visibilitychange", () => {
406
+ if (document.visibilityState === "hidden") {
407
+ const duration = Math.round((Date.now() - this.pageEntryTime) / 1e3);
408
+ if (duration > 0) {
409
+ this.trackEvent("page_exit", {
410
+ path: this.currentPagePath,
411
+ durationSeconds: duration
412
+ });
413
+ this.flushAnalytics();
414
+ }
415
+ } else if (document.visibilityState === "visible") {
416
+ this.pageEntryTime = Date.now();
417
+ }
418
+ });
419
+ }
420
+ trackPageView() {
421
+ const props = {
422
+ path: this.currentPagePath,
423
+ title: document.title,
424
+ referrer: document.referrer || "",
425
+ isLoggedIn: !!this.config.userId
426
+ };
427
+ if (this.config.userId) props.userId = this.config.userId;
428
+ if (this.config.userEmail) props.userEmail = this.config.userEmail;
429
+ this.trackEvent("page_view", props);
430
+ }
431
+ onNavigate() {
432
+ const newPath = window.location.pathname;
433
+ if (this.currentPagePath && this.pageEntryTime > 0) {
434
+ const duration = Math.round((Date.now() - this.pageEntryTime) / 1e3);
435
+ if (duration > 0) {
436
+ this.trackEvent("page_exit", {
437
+ path: this.currentPagePath,
438
+ durationSeconds: duration
439
+ });
440
+ }
441
+ }
442
+ this.currentPagePath = newPath;
443
+ this.pageEntryTime = Date.now();
444
+ setTimeout(() => this.trackPageView(), 100);
445
+ }
367
446
  generateSessionId() {
368
447
  if (typeof crypto !== "undefined" && crypto.getRandomValues) {
369
448
  const array = new Uint8Array(16);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "featurely-site-manager",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
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",