featurely-site-manager 1.1.27 → 1.1.29

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
@@ -1,6 +1,6 @@
1
1
  # featurely-site-manager
2
2
 
3
- Powerful site management SDK for controlling maintenance mode, displaying status messages, and managing site-wide features from your Featurely dashboard.
3
+ Powerful site management SDK for controlling maintenance mode, displaying status messages, managing site-wide features, and scanning your project's dependencies for vulnerabilities — all from your Featurely dashboard.
4
4
 
5
5
  ## 📦 Installation
6
6
 
@@ -8,7 +8,63 @@ Powerful site management SDK for controlling maintenance mode, displaying status
8
8
  npm install featurely-site-manager
9
9
  ```
10
10
 
11
- ## 🚀 Quick Start
11
+ ## 🔍 Package Security Scanning (CLI)
12
+
13
+ Scan your project's dependencies for outdated versions, known CVEs, and breaking changes. Results appear instantly in your Featurely dashboard under **Package security**.
14
+
15
+ ### Run a scan
16
+
17
+ ```bash
18
+ npx featurely-site-manager scan packages \
19
+ --key YOUR_API_KEY \
20
+ --project YOUR_PROJECT_ID
21
+ ```
22
+
23
+ Run this from the directory that contains your `package.json`. After the first scan your package list is stored — use the **Re-scan** button in the dashboard to re-check without the CLI.
24
+
25
+ ### What gets reported
26
+
27
+ | Check | Source |
28
+ | --------------------------------------------- | -------------------------------------------- |
29
+ | Installed vs latest version | npm registry |
30
+ | Breaking change detection (major bump) | semver comparison |
31
+ | Known CVEs — critical / high / moderate / low | [OSV database](https://osv.dev) (GHSA + NVD) |
32
+ | Deprecated packages | npm registry |
33
+ | License (SPDX) | npm registry |
34
+
35
+ ### GitHub Actions — automatic scanning
36
+
37
+ Add to `.github/workflows/featurely-scan.yml` and set `FEATURELY_API_KEY` + `FEATURELY_PROJECT_ID` in your repository secrets.
38
+
39
+ ```yaml
40
+ name: Featurely package scan
41
+
42
+ on:
43
+ push:
44
+ branches: [main]
45
+ paths:
46
+ - "package.json"
47
+ - "package-lock.json"
48
+ - "yarn.lock"
49
+ - "pnpm-lock.yaml"
50
+ workflow_dispatch:
51
+
52
+ jobs:
53
+ scan:
54
+ runs-on: ubuntu-latest
55
+ steps:
56
+ - uses: actions/checkout@v4
57
+ - uses: actions/setup-node@v4
58
+ with:
59
+ node-version: "20"
60
+ - name: Run Featurely package scan
61
+ run: |
62
+ npx featurely-site-manager scan packages \
63
+ --key ${{ secrets.FEATURELY_API_KEY }} \
64
+ --project ${{ secrets.FEATURELY_PROJECT_ID }}
65
+ ```
66
+
67
+ ## 🚀 Quick Start (SDK)
12
68
 
13
69
  ```typescript
14
70
  import { SiteManager } from "featurely-site-manager";
package/dist/index.d.mts CHANGED
@@ -1,3 +1,4 @@
1
+ declare const SDK_VERSION = "1.1.28";
1
2
  type MessageType = "info" | "warning" | "error" | "success";
2
3
  type MessagePosition = "top" | "bottom";
3
4
  type MessageStyle = "banner" | "toast";
@@ -76,11 +77,33 @@ interface VersionUpdateRules {
76
77
  minor?: "required" | "available";
77
78
  patch?: "required" | "recommended" | "available";
78
79
  }
80
+ interface AnalyticsConfig {
81
+ urlWhitelist?: string[];
82
+ urlBlacklist?: string[];
83
+ userWhitelist?: string[];
84
+ userBlacklist?: string[];
85
+ sampleRate?: number;
86
+ trackPageViews?: boolean;
87
+ trackWebVitals?: boolean;
88
+ trackOutboundLinks?: boolean;
89
+ trackSessionStart?: boolean;
90
+ flushInterval?: number;
91
+ }
92
+ interface SdkVersionRequirement {
93
+ packageName: string;
94
+ displayName: string;
95
+ currentVersion: string;
96
+ minimumVersion: string;
97
+ updateCommand: string;
98
+ changelog?: string;
99
+ }
79
100
  interface SiteConfig {
80
101
  maintenance: MaintenanceConfig;
81
102
  messages: StatusMessage[];
82
103
  featureFlags: FeatureFlag[];
83
104
  lastUpdated: string;
105
+ analyticsConfig?: AnalyticsConfig | null;
106
+ sdkVersions?: SdkVersionRequirement[];
84
107
  }
85
108
  interface SiteManagerConfig {
86
109
  apiKey: string;
@@ -117,6 +140,9 @@ declare class SiteManager {
117
140
  private config;
118
141
  private siteConfig;
119
142
  private configETag;
143
+ private analyticsConfig;
144
+ private _sessionSampled;
145
+ private _sdkUpdateToastShown;
120
146
  private pollIntervalId;
121
147
  private versionCheckIntervalId;
122
148
  private messageContainers;
@@ -166,6 +192,9 @@ declare class SiteManager {
166
192
  isInMaintenanceMode(): boolean;
167
193
  getActiveMessages(): StatusMessage[];
168
194
  refresh(): Promise<void>;
195
+ private matchesPattern;
196
+ private matchesAnyPattern;
197
+ private isSessionSampled;
169
198
  trackEvent(eventName: string, properties?: Record<string, string | number | boolean>): void;
170
199
  track404(path?: string): void;
171
200
  private fetchConfig;
@@ -190,6 +219,8 @@ declare class SiteManager {
190
219
  private trackPageExit;
191
220
  private onNavigate;
192
221
  private generateSessionId;
222
+ private compareVersions;
223
+ private showSdkUpdateToast;
193
224
  checkVersion(currentVersion?: string): Promise<VersionCheckResponse | null>;
194
225
  forceUpdateWeb(): Promise<void>;
195
226
  getLastVersionCheck(): VersionCheckResponse | null;
@@ -221,4 +252,4 @@ declare class SiteManager {
221
252
  private injectStyles;
222
253
  }
223
254
 
224
- export { type AppVersion, type FeatureFlag, type MaintenanceConfig, type MessagePosition, type MessageStyle, type MessageType, type SiteConfig, SiteManager, type SiteManagerConfig, type StatusMessage, type VersionCheckResponse, type VersionUpdateRules, SiteManager as default };
255
+ export { type AnalyticsConfig, type AppVersion, type FeatureFlag, type MaintenanceConfig, type MessagePosition, type MessageStyle, type MessageType, SDK_VERSION, type SdkVersionRequirement, type SiteConfig, SiteManager, type SiteManagerConfig, type StatusMessage, type VersionCheckResponse, type VersionUpdateRules, SiteManager as default };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ declare const SDK_VERSION = "1.1.28";
1
2
  type MessageType = "info" | "warning" | "error" | "success";
2
3
  type MessagePosition = "top" | "bottom";
3
4
  type MessageStyle = "banner" | "toast";
@@ -76,11 +77,33 @@ interface VersionUpdateRules {
76
77
  minor?: "required" | "available";
77
78
  patch?: "required" | "recommended" | "available";
78
79
  }
80
+ interface AnalyticsConfig {
81
+ urlWhitelist?: string[];
82
+ urlBlacklist?: string[];
83
+ userWhitelist?: string[];
84
+ userBlacklist?: string[];
85
+ sampleRate?: number;
86
+ trackPageViews?: boolean;
87
+ trackWebVitals?: boolean;
88
+ trackOutboundLinks?: boolean;
89
+ trackSessionStart?: boolean;
90
+ flushInterval?: number;
91
+ }
92
+ interface SdkVersionRequirement {
93
+ packageName: string;
94
+ displayName: string;
95
+ currentVersion: string;
96
+ minimumVersion: string;
97
+ updateCommand: string;
98
+ changelog?: string;
99
+ }
79
100
  interface SiteConfig {
80
101
  maintenance: MaintenanceConfig;
81
102
  messages: StatusMessage[];
82
103
  featureFlags: FeatureFlag[];
83
104
  lastUpdated: string;
105
+ analyticsConfig?: AnalyticsConfig | null;
106
+ sdkVersions?: SdkVersionRequirement[];
84
107
  }
85
108
  interface SiteManagerConfig {
86
109
  apiKey: string;
@@ -117,6 +140,9 @@ declare class SiteManager {
117
140
  private config;
118
141
  private siteConfig;
119
142
  private configETag;
143
+ private analyticsConfig;
144
+ private _sessionSampled;
145
+ private _sdkUpdateToastShown;
120
146
  private pollIntervalId;
121
147
  private versionCheckIntervalId;
122
148
  private messageContainers;
@@ -166,6 +192,9 @@ declare class SiteManager {
166
192
  isInMaintenanceMode(): boolean;
167
193
  getActiveMessages(): StatusMessage[];
168
194
  refresh(): Promise<void>;
195
+ private matchesPattern;
196
+ private matchesAnyPattern;
197
+ private isSessionSampled;
169
198
  trackEvent(eventName: string, properties?: Record<string, string | number | boolean>): void;
170
199
  track404(path?: string): void;
171
200
  private fetchConfig;
@@ -190,6 +219,8 @@ declare class SiteManager {
190
219
  private trackPageExit;
191
220
  private onNavigate;
192
221
  private generateSessionId;
222
+ private compareVersions;
223
+ private showSdkUpdateToast;
193
224
  checkVersion(currentVersion?: string): Promise<VersionCheckResponse | null>;
194
225
  forceUpdateWeb(): Promise<void>;
195
226
  getLastVersionCheck(): VersionCheckResponse | null;
@@ -221,4 +252,4 @@ declare class SiteManager {
221
252
  private injectStyles;
222
253
  }
223
254
 
224
- export { type AppVersion, type FeatureFlag, type MaintenanceConfig, type MessagePosition, type MessageStyle, type MessageType, type SiteConfig, SiteManager, type SiteManagerConfig, type StatusMessage, type VersionCheckResponse, type VersionUpdateRules, SiteManager as default };
255
+ export { type AnalyticsConfig, type AppVersion, type FeatureFlag, type MaintenanceConfig, type MessagePosition, type MessageStyle, type MessageType, SDK_VERSION, type SdkVersionRequirement, type SiteConfig, SiteManager, type SiteManagerConfig, type StatusMessage, type VersionCheckResponse, type VersionUpdateRules, SiteManager as default };
package/dist/index.js CHANGED
@@ -30,15 +30,20 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ SDK_VERSION: () => SDK_VERSION,
33
34
  SiteManager: () => SiteManager,
34
35
  default: () => index_default
35
36
  });
36
37
  module.exports = __toCommonJS(index_exports);
37
38
  var import_dompurify = __toESM(require("dompurify"));
39
+ var SDK_VERSION = "1.1.28";
38
40
  var _SiteManager = class _SiteManager {
39
41
  constructor(config) {
40
42
  this.siteConfig = null;
41
43
  this.configETag = null;
44
+ this.analyticsConfig = null;
45
+ this._sessionSampled = null;
46
+ this._sdkUpdateToastShown = false;
42
47
  this.pollIntervalId = null;
43
48
  this.versionCheckIntervalId = null;
44
49
  this.messageContainers = /* @__PURE__ */ new Map();
@@ -147,7 +152,7 @@ var _SiteManager = class _SiteManager {
147
152
  }
148
153
  this.debugLog(
149
154
  "info",
150
- `[init] v1.1.19 | project: ${this.config.projectId} | hostname: ${typeof window !== "undefined" ? window.location.hostname : "\u2014"}`
155
+ `[init] v${SDK_VERSION} | project: ${this.config.projectId} | hostname: ${typeof window !== "undefined" ? window.location.hostname : "\u2014"}`
151
156
  );
152
157
  this.debugLog(
153
158
  "info",
@@ -409,10 +414,56 @@ var _SiteManager = class _SiteManager {
409
414
  * @param eventName - Name of the event (e.g., 'button_clicked', 'feature_used')
410
415
  * @param properties - Optional event properties (e.g., { button: 'signup', page: '/home' })
411
416
  */
417
+ /** Returns true if `path` matches the given glob pattern (supports * wildcard). */
418
+ matchesPattern(path, pattern) {
419
+ if (!pattern) return false;
420
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
421
+ try {
422
+ return new RegExp(`^${escaped}$`).test(path);
423
+ } catch {
424
+ return path === pattern;
425
+ }
426
+ }
427
+ /** Returns true if `path` matches any of the given glob patterns. */
428
+ matchesAnyPattern(path, patterns) {
429
+ return patterns.some((p) => this.matchesPattern(path, p));
430
+ }
431
+ /** Returns true if the current session should be tracked (respects sample rate). */
432
+ isSessionSampled() {
433
+ var _a, _b;
434
+ if (this._sessionSampled !== null) return this._sessionSampled;
435
+ const rate = (_b = (_a = this.analyticsConfig) == null ? void 0 : _a.sampleRate) != null ? _b : 100;
436
+ this._sessionSampled = rate >= 100 || Math.random() * 100 < rate;
437
+ return this._sessionSampled;
438
+ }
412
439
  trackEvent(eventName, properties) {
440
+ var _a, _b, _c, _d;
413
441
  if (!this.config.enableAnalytics) {
414
442
  return;
415
443
  }
444
+ const ac = this.analyticsConfig;
445
+ if (ac) {
446
+ if (!this.isSessionSampled()) return;
447
+ if (eventName === "session_start" && ac.trackSessionStart === false) return;
448
+ if ((eventName === "page_view" || eventName === "page_exit") && ac.trackPageViews === false) return;
449
+ if (eventName === "web_vital" && ac.trackWebVitals === false) return;
450
+ if (eventName === "outbound_link_click" && ac.trackOutboundLinks === false) return;
451
+ const urlEvents = ["page_view", "page_exit", "page_not_found"];
452
+ if (urlEvents.includes(eventName)) {
453
+ const path = (properties == null ? void 0 : properties.path) || (typeof window !== "undefined" ? window.location.pathname : "");
454
+ if (((_a = ac.urlBlacklist) == null ? void 0 : _a.length) && this.matchesAnyPattern(path, ac.urlBlacklist)) return;
455
+ if (((_b = ac.urlWhitelist) == null ? void 0 : _b.length) && !this.matchesAnyPattern(path, ac.urlWhitelist)) return;
456
+ }
457
+ const userId = this.config.userId || "";
458
+ const userEmail = this.config.userEmail || "";
459
+ if ((_c = ac.userBlacklist) == null ? void 0 : _c.length) {
460
+ if (userId && ac.userBlacklist.includes(userId) || userEmail && ac.userBlacklist.includes(userEmail)) return;
461
+ }
462
+ if ((_d = ac.userWhitelist) == null ? void 0 : _d.length) {
463
+ const inWhitelist = userId && ac.userWhitelist.includes(userId) || userEmail && ac.userWhitelist.includes(userEmail);
464
+ if (!inWhitelist) return;
465
+ }
466
+ }
416
467
  this.analyticsQueue.push({
417
468
  eventName,
418
469
  properties,
@@ -443,7 +494,7 @@ var _SiteManager = class _SiteManager {
443
494
  // Configuration Fetching
444
495
  // ============================================================================
445
496
  async fetchConfig() {
446
- var _a, _b, _c, _d, _e, _f, _g, _h;
497
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
447
498
  try {
448
499
  const fetchStartMs = Date.now();
449
500
  const headers = {
@@ -527,6 +578,27 @@ var _SiteManager = class _SiteManager {
527
578
  }
528
579
  }
529
580
  this.siteConfig = newConfig;
581
+ if (newConfig.analyticsConfig !== void 0) {
582
+ this.analyticsConfig = (_i = newConfig.analyticsConfig) != null ? _i : null;
583
+ const serverFlushInterval = (_j = this.analyticsConfig) == null ? void 0 : _j.flushInterval;
584
+ if (serverFlushInterval && serverFlushInterval !== this.config.analyticsFlushInterval) {
585
+ this.config.analyticsFlushInterval = serverFlushInterval;
586
+ if (this.analyticsFlushIntervalId) {
587
+ clearInterval(this.analyticsFlushIntervalId);
588
+ this.analyticsFlushIntervalId = null;
589
+ }
590
+ this.startAnalyticsFlushing();
591
+ this.debugLog("info", `[analytics] flush interval updated to ${serverFlushInterval}ms from server config`);
592
+ }
593
+ }
594
+ if (!this._sdkUpdateToastShown && ((_k = newConfig.sdkVersions) == null ? void 0 : _k.length)) {
595
+ const req = newConfig.sdkVersions.find((s) => s.packageName === "featurely-site-manager");
596
+ if (req && req.minimumVersion && this.compareVersions(SDK_VERSION, req.minimumVersion) < 0) {
597
+ this._sdkUpdateToastShown = true;
598
+ this.showSdkUpdateToast(SDK_VERSION, req.minimumVersion, req.currentVersion, req.updateCommand, req.changelog);
599
+ this.debugLog("warn", `[sdk] version ${SDK_VERSION} is below minimum ${req.minimumVersion} \u2014 update toast shown`);
600
+ }
601
+ }
530
602
  const isGlobalDebug = newConfig.debugMode === true;
531
603
  const isEnvDebug = (matchedEnv == null ? void 0 : matchedEnv.debugEnabled) === true;
532
604
  const shouldDebug = isGlobalDebug || isEnvDebug;
@@ -649,7 +721,6 @@ var _SiteManager = class _SiteManager {
649
721
  if (this.analyticsQueue.length === 0) {
650
722
  return;
651
723
  }
652
- const url = `${this.config.apiUrl}/api/projects/${this.config.projectId}/analytics/events`;
653
724
  const eventsToSend = [...this.analyticsQueue];
654
725
  this.analyticsQueue = [];
655
726
  this.analyticsEventsSent += eventsToSend.length;
@@ -657,61 +728,62 @@ var _SiteManager = class _SiteManager {
657
728
  "info",
658
729
  `[analytics] flushing ${eventsToSend.length} event(s) (total sent: ${this.analyticsEventsSent})`
659
730
  );
660
- for (const event of eventsToSend) {
661
- const payload = JSON.stringify({
731
+ const isBatch = eventsToSend.length > 1;
732
+ const url = isBatch ? `${this.config.apiUrl}/api/projects/${this.config.projectId}/analytics/events/batch` : `${this.config.apiUrl}/api/projects/${this.config.projectId}/analytics/events`;
733
+ const commonFields = {
734
+ userId: this.config.userId,
735
+ sessionId: this.sessionId,
736
+ visitorId: this.visitorId || void 0,
737
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0,
738
+ platform: typeof navigator !== "undefined" ? navigator.platform : void 0,
739
+ appVersion: this.config.appVersion
740
+ };
741
+ const payload = isBatch ? JSON.stringify({
742
+ events: eventsToSend.map((event) => ({
662
743
  eventName: event.eventName,
663
744
  properties: event.properties,
664
- userId: this.config.userId,
665
- sessionId: this.sessionId,
666
- visitorId: this.visitorId || void 0,
667
- userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0,
668
- platform: typeof navigator !== "undefined" ? navigator.platform : void 0,
669
- appVersion: this.config.appVersion
670
- });
671
- if (useBeacon) {
672
- if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
673
- navigator.sendBeacon(url, payload);
745
+ timestamp: event.timestamp,
746
+ ...commonFields
747
+ }))
748
+ }) : JSON.stringify({
749
+ eventName: eventsToSend[0].eventName,
750
+ properties: eventsToSend[0].properties,
751
+ ...commonFields
752
+ });
753
+ const shortPath = isBatch ? `/api/projects/${this.config.projectId}/analytics/events/batch` : `/api/projects/${this.config.projectId}/analytics/events`;
754
+ if (useBeacon) {
755
+ if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
756
+ navigator.sendBeacon(url, payload);
757
+ }
758
+ } else {
759
+ try {
760
+ const t0 = Date.now();
761
+ const res = await fetch(url, {
762
+ method: "POST",
763
+ headers: { "Content-Type": "application/json" },
764
+ body: payload,
765
+ keepalive: true
766
+ });
767
+ this.debugNetwork(shortPath, res.status, Date.now() - t0);
768
+ if (!res.ok) {
769
+ const body = await res.text().catch(() => "(unreadable)");
770
+ const errDetail = `[analytics] batch rejected: ${res.status} ${res.statusText} \u2014 ${body}`;
771
+ console.error(`[Featurely] ${errDetail}`);
772
+ this.debugLog("error", errDetail);
773
+ this.errorCount++;
674
774
  }
675
- } else {
676
- try {
677
- const t0 = Date.now();
678
- const res = await fetch(url, {
679
- method: "POST",
680
- headers: { "Content-Type": "application/json" },
681
- body: payload,
682
- keepalive: true
683
- });
684
- this.debugNetwork(
685
- `/api/projects/${this.config.projectId}/analytics/events`,
686
- res.status,
687
- Date.now() - t0
775
+ } catch (fetchError) {
776
+ const sentViaBeacon = typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && navigator.sendBeacon(url, payload);
777
+ const errMsg = fetchError instanceof Error ? fetchError.message : String(fetchError);
778
+ if (!sentViaBeacon) {
779
+ console.error(`[Featurely] Failed to send analytics batch:`, fetchError);
780
+ this.debugLog("error", `[analytics] batch send failed: ${errMsg}`);
781
+ this.errorCount++;
782
+ } else {
783
+ this.debugLog(
784
+ "warn",
785
+ `[analytics] fetch failed (${errMsg}), delivered via sendBeacon`
688
786
  );
689
- if (!res.ok) {
690
- const body = await res.text().catch(() => "(unreadable)");
691
- const errDetail = `[analytics] event "${event.eventName}" rejected: ${res.status} ${res.statusText} \u2014 ${body}`;
692
- console.error(`[Featurely] ${errDetail}`);
693
- this.debugLog("error", errDetail);
694
- this.errorCount++;
695
- }
696
- } catch (fetchError) {
697
- const sentViaBeacon = typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && navigator.sendBeacon(url, payload);
698
- const errMsg = fetchError instanceof Error ? fetchError.message : String(fetchError);
699
- if (!sentViaBeacon) {
700
- console.error(
701
- `[Featurely] Failed to send analytics event "${event.eventName}":`,
702
- fetchError
703
- );
704
- this.debugLog(
705
- "error",
706
- `[analytics] send failed for "${event.eventName}": ${errMsg}`
707
- );
708
- this.errorCount++;
709
- } else {
710
- this.debugLog(
711
- "warn",
712
- `[analytics] fetch failed for "${event.eventName}" (${errMsg}), delivered via sendBeacon`
713
- );
714
- }
715
787
  }
716
788
  }
717
789
  }
@@ -1331,6 +1403,69 @@ var _SiteManager = class _SiteManager {
1331
1403
  }
1332
1404
  return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
1333
1405
  }
1406
+ /**
1407
+ * Compare two semver strings. Returns -1 if a < b, 0 if equal, 1 if a > b.
1408
+ * Handles "major.minor.patch" format.
1409
+ */
1410
+ compareVersions(a, b) {
1411
+ const parse = (v) => v.split(".").map((n) => parseInt(n, 10) || 0);
1412
+ const [aMaj, aMin, aPat] = parse(a);
1413
+ const [bMaj, bMin, bPat] = parse(b);
1414
+ if (aMaj !== bMaj) return aMaj < bMaj ? -1 : 1;
1415
+ if (aMin !== bMin) return aMin < bMin ? -1 : 1;
1416
+ if (aPat !== bPat) return aPat < bPat ? -1 : 1;
1417
+ return 0;
1418
+ }
1419
+ /**
1420
+ * Show a dismissible toast notification prompting developers to update the SDK.
1421
+ * This only appears in browser DevTools / on the page when the SDK is outdated.
1422
+ */
1423
+ showSdkUpdateToast(currentVersion, minimumVersion, latestVersion, updateCommand, changelog) {
1424
+ if (typeof document === "undefined") return;
1425
+ const existing = document.getElementById("ft-sdk-update-toast");
1426
+ if (existing) existing.remove();
1427
+ const toast = document.createElement("div");
1428
+ toast.id = "ft-sdk-update-toast";
1429
+ Object.assign(toast.style, {
1430
+ position: "fixed",
1431
+ bottom: "16px",
1432
+ right: "16px",
1433
+ zIndex: "2147483647",
1434
+ background: "#1c1917",
1435
+ color: "#fafaf9",
1436
+ borderLeft: "4px solid #f59e0b",
1437
+ borderRadius: "8px",
1438
+ padding: "14px 16px",
1439
+ maxWidth: "360px",
1440
+ width: "calc(100vw - 32px)",
1441
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
1442
+ fontSize: "13px",
1443
+ boxShadow: "0 4px 32px rgba(0,0,0,0.4)",
1444
+ lineHeight: "1.5",
1445
+ fontWeight: "400"
1446
+ });
1447
+ const changelogHtml = changelog ? `<p style="margin:6px 0 0;color:#a8a29e;font-size:12px;">${changelog}</p>` : "";
1448
+ toast.innerHTML = `
1449
+ <button id="ft-sdk-update-close" style="position:absolute;top:10px;right:10px;background:none;border:none;cursor:pointer;color:#78716c;font-size:16px;line-height:1;padding:2px 4px;" aria-label="Dismiss">\xD7</button>
1450
+ <p style="margin:0 20px 0 0;font-weight:600;color:#fbbf24;">\u{1F4E6} SDK update required</p>
1451
+ <p style="margin:6px 0 0;color:#a8a29e;font-size:12px;">
1452
+ <strong style="color:#fafaf9;">featurely-site-manager</strong>
1453
+ v${currentVersion} is below the minimum v${minimumVersion} (latest: v${latestVersion})
1454
+ </p>
1455
+ ${changelogHtml}
1456
+ <p style="margin:8px 0 0;">
1457
+ <code style="background:#292524;border-radius:4px;padding:3px 7px;font-size:11px;color:#d6d3d1;font-family:monospace;word-break:break-all;">${updateCommand}</code>
1458
+ </p>
1459
+ `;
1460
+ document.body.appendChild(toast);
1461
+ const closeBtn = document.getElementById("ft-sdk-update-close");
1462
+ if (closeBtn) {
1463
+ closeBtn.addEventListener("click", () => toast.remove());
1464
+ }
1465
+ setTimeout(() => {
1466
+ if (toast.parentNode) toast.remove();
1467
+ }, 6e4);
1468
+ }
1334
1469
  // ============================================================================
1335
1470
  // Version Checking
1336
1471
  // ============================================================================
@@ -2226,5 +2361,6 @@ var SiteManager = _SiteManager;
2226
2361
  var index_default = SiteManager;
2227
2362
  // Annotate the CommonJS export names for ESM import in node:
2228
2363
  0 && (module.exports = {
2364
+ SDK_VERSION,
2229
2365
  SiteManager
2230
2366
  });
package/dist/index.mjs CHANGED
@@ -1,9 +1,13 @@
1
1
  // src/index.ts
2
2
  import DOMPurify from "dompurify";
3
+ var SDK_VERSION = "1.1.28";
3
4
  var _SiteManager = class _SiteManager {
4
5
  constructor(config) {
5
6
  this.siteConfig = null;
6
7
  this.configETag = null;
8
+ this.analyticsConfig = null;
9
+ this._sessionSampled = null;
10
+ this._sdkUpdateToastShown = false;
7
11
  this.pollIntervalId = null;
8
12
  this.versionCheckIntervalId = null;
9
13
  this.messageContainers = /* @__PURE__ */ new Map();
@@ -112,7 +116,7 @@ var _SiteManager = class _SiteManager {
112
116
  }
113
117
  this.debugLog(
114
118
  "info",
115
- `[init] v1.1.19 | project: ${this.config.projectId} | hostname: ${typeof window !== "undefined" ? window.location.hostname : "\u2014"}`
119
+ `[init] v${SDK_VERSION} | project: ${this.config.projectId} | hostname: ${typeof window !== "undefined" ? window.location.hostname : "\u2014"}`
116
120
  );
117
121
  this.debugLog(
118
122
  "info",
@@ -374,10 +378,56 @@ var _SiteManager = class _SiteManager {
374
378
  * @param eventName - Name of the event (e.g., 'button_clicked', 'feature_used')
375
379
  * @param properties - Optional event properties (e.g., { button: 'signup', page: '/home' })
376
380
  */
381
+ /** Returns true if `path` matches the given glob pattern (supports * wildcard). */
382
+ matchesPattern(path, pattern) {
383
+ if (!pattern) return false;
384
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
385
+ try {
386
+ return new RegExp(`^${escaped}$`).test(path);
387
+ } catch {
388
+ return path === pattern;
389
+ }
390
+ }
391
+ /** Returns true if `path` matches any of the given glob patterns. */
392
+ matchesAnyPattern(path, patterns) {
393
+ return patterns.some((p) => this.matchesPattern(path, p));
394
+ }
395
+ /** Returns true if the current session should be tracked (respects sample rate). */
396
+ isSessionSampled() {
397
+ var _a, _b;
398
+ if (this._sessionSampled !== null) return this._sessionSampled;
399
+ const rate = (_b = (_a = this.analyticsConfig) == null ? void 0 : _a.sampleRate) != null ? _b : 100;
400
+ this._sessionSampled = rate >= 100 || Math.random() * 100 < rate;
401
+ return this._sessionSampled;
402
+ }
377
403
  trackEvent(eventName, properties) {
404
+ var _a, _b, _c, _d;
378
405
  if (!this.config.enableAnalytics) {
379
406
  return;
380
407
  }
408
+ const ac = this.analyticsConfig;
409
+ if (ac) {
410
+ if (!this.isSessionSampled()) return;
411
+ if (eventName === "session_start" && ac.trackSessionStart === false) return;
412
+ if ((eventName === "page_view" || eventName === "page_exit") && ac.trackPageViews === false) return;
413
+ if (eventName === "web_vital" && ac.trackWebVitals === false) return;
414
+ if (eventName === "outbound_link_click" && ac.trackOutboundLinks === false) return;
415
+ const urlEvents = ["page_view", "page_exit", "page_not_found"];
416
+ if (urlEvents.includes(eventName)) {
417
+ const path = (properties == null ? void 0 : properties.path) || (typeof window !== "undefined" ? window.location.pathname : "");
418
+ if (((_a = ac.urlBlacklist) == null ? void 0 : _a.length) && this.matchesAnyPattern(path, ac.urlBlacklist)) return;
419
+ if (((_b = ac.urlWhitelist) == null ? void 0 : _b.length) && !this.matchesAnyPattern(path, ac.urlWhitelist)) return;
420
+ }
421
+ const userId = this.config.userId || "";
422
+ const userEmail = this.config.userEmail || "";
423
+ if ((_c = ac.userBlacklist) == null ? void 0 : _c.length) {
424
+ if (userId && ac.userBlacklist.includes(userId) || userEmail && ac.userBlacklist.includes(userEmail)) return;
425
+ }
426
+ if ((_d = ac.userWhitelist) == null ? void 0 : _d.length) {
427
+ const inWhitelist = userId && ac.userWhitelist.includes(userId) || userEmail && ac.userWhitelist.includes(userEmail);
428
+ if (!inWhitelist) return;
429
+ }
430
+ }
381
431
  this.analyticsQueue.push({
382
432
  eventName,
383
433
  properties,
@@ -408,7 +458,7 @@ var _SiteManager = class _SiteManager {
408
458
  // Configuration Fetching
409
459
  // ============================================================================
410
460
  async fetchConfig() {
411
- var _a, _b, _c, _d, _e, _f, _g, _h;
461
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
412
462
  try {
413
463
  const fetchStartMs = Date.now();
414
464
  const headers = {
@@ -492,6 +542,27 @@ var _SiteManager = class _SiteManager {
492
542
  }
493
543
  }
494
544
  this.siteConfig = newConfig;
545
+ if (newConfig.analyticsConfig !== void 0) {
546
+ this.analyticsConfig = (_i = newConfig.analyticsConfig) != null ? _i : null;
547
+ const serverFlushInterval = (_j = this.analyticsConfig) == null ? void 0 : _j.flushInterval;
548
+ if (serverFlushInterval && serverFlushInterval !== this.config.analyticsFlushInterval) {
549
+ this.config.analyticsFlushInterval = serverFlushInterval;
550
+ if (this.analyticsFlushIntervalId) {
551
+ clearInterval(this.analyticsFlushIntervalId);
552
+ this.analyticsFlushIntervalId = null;
553
+ }
554
+ this.startAnalyticsFlushing();
555
+ this.debugLog("info", `[analytics] flush interval updated to ${serverFlushInterval}ms from server config`);
556
+ }
557
+ }
558
+ if (!this._sdkUpdateToastShown && ((_k = newConfig.sdkVersions) == null ? void 0 : _k.length)) {
559
+ const req = newConfig.sdkVersions.find((s) => s.packageName === "featurely-site-manager");
560
+ if (req && req.minimumVersion && this.compareVersions(SDK_VERSION, req.minimumVersion) < 0) {
561
+ this._sdkUpdateToastShown = true;
562
+ this.showSdkUpdateToast(SDK_VERSION, req.minimumVersion, req.currentVersion, req.updateCommand, req.changelog);
563
+ this.debugLog("warn", `[sdk] version ${SDK_VERSION} is below minimum ${req.minimumVersion} \u2014 update toast shown`);
564
+ }
565
+ }
495
566
  const isGlobalDebug = newConfig.debugMode === true;
496
567
  const isEnvDebug = (matchedEnv == null ? void 0 : matchedEnv.debugEnabled) === true;
497
568
  const shouldDebug = isGlobalDebug || isEnvDebug;
@@ -614,7 +685,6 @@ var _SiteManager = class _SiteManager {
614
685
  if (this.analyticsQueue.length === 0) {
615
686
  return;
616
687
  }
617
- const url = `${this.config.apiUrl}/api/projects/${this.config.projectId}/analytics/events`;
618
688
  const eventsToSend = [...this.analyticsQueue];
619
689
  this.analyticsQueue = [];
620
690
  this.analyticsEventsSent += eventsToSend.length;
@@ -622,61 +692,62 @@ var _SiteManager = class _SiteManager {
622
692
  "info",
623
693
  `[analytics] flushing ${eventsToSend.length} event(s) (total sent: ${this.analyticsEventsSent})`
624
694
  );
625
- for (const event of eventsToSend) {
626
- const payload = JSON.stringify({
695
+ const isBatch = eventsToSend.length > 1;
696
+ const url = isBatch ? `${this.config.apiUrl}/api/projects/${this.config.projectId}/analytics/events/batch` : `${this.config.apiUrl}/api/projects/${this.config.projectId}/analytics/events`;
697
+ const commonFields = {
698
+ userId: this.config.userId,
699
+ sessionId: this.sessionId,
700
+ visitorId: this.visitorId || void 0,
701
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0,
702
+ platform: typeof navigator !== "undefined" ? navigator.platform : void 0,
703
+ appVersion: this.config.appVersion
704
+ };
705
+ const payload = isBatch ? JSON.stringify({
706
+ events: eventsToSend.map((event) => ({
627
707
  eventName: event.eventName,
628
708
  properties: event.properties,
629
- userId: this.config.userId,
630
- sessionId: this.sessionId,
631
- visitorId: this.visitorId || void 0,
632
- userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0,
633
- platform: typeof navigator !== "undefined" ? navigator.platform : void 0,
634
- appVersion: this.config.appVersion
635
- });
636
- if (useBeacon) {
637
- if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
638
- navigator.sendBeacon(url, payload);
709
+ timestamp: event.timestamp,
710
+ ...commonFields
711
+ }))
712
+ }) : JSON.stringify({
713
+ eventName: eventsToSend[0].eventName,
714
+ properties: eventsToSend[0].properties,
715
+ ...commonFields
716
+ });
717
+ const shortPath = isBatch ? `/api/projects/${this.config.projectId}/analytics/events/batch` : `/api/projects/${this.config.projectId}/analytics/events`;
718
+ if (useBeacon) {
719
+ if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
720
+ navigator.sendBeacon(url, payload);
721
+ }
722
+ } else {
723
+ try {
724
+ const t0 = Date.now();
725
+ const res = await fetch(url, {
726
+ method: "POST",
727
+ headers: { "Content-Type": "application/json" },
728
+ body: payload,
729
+ keepalive: true
730
+ });
731
+ this.debugNetwork(shortPath, res.status, Date.now() - t0);
732
+ if (!res.ok) {
733
+ const body = await res.text().catch(() => "(unreadable)");
734
+ const errDetail = `[analytics] batch rejected: ${res.status} ${res.statusText} \u2014 ${body}`;
735
+ console.error(`[Featurely] ${errDetail}`);
736
+ this.debugLog("error", errDetail);
737
+ this.errorCount++;
639
738
  }
640
- } else {
641
- try {
642
- const t0 = Date.now();
643
- const res = await fetch(url, {
644
- method: "POST",
645
- headers: { "Content-Type": "application/json" },
646
- body: payload,
647
- keepalive: true
648
- });
649
- this.debugNetwork(
650
- `/api/projects/${this.config.projectId}/analytics/events`,
651
- res.status,
652
- Date.now() - t0
739
+ } catch (fetchError) {
740
+ const sentViaBeacon = typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && navigator.sendBeacon(url, payload);
741
+ const errMsg = fetchError instanceof Error ? fetchError.message : String(fetchError);
742
+ if (!sentViaBeacon) {
743
+ console.error(`[Featurely] Failed to send analytics batch:`, fetchError);
744
+ this.debugLog("error", `[analytics] batch send failed: ${errMsg}`);
745
+ this.errorCount++;
746
+ } else {
747
+ this.debugLog(
748
+ "warn",
749
+ `[analytics] fetch failed (${errMsg}), delivered via sendBeacon`
653
750
  );
654
- if (!res.ok) {
655
- const body = await res.text().catch(() => "(unreadable)");
656
- const errDetail = `[analytics] event "${event.eventName}" rejected: ${res.status} ${res.statusText} \u2014 ${body}`;
657
- console.error(`[Featurely] ${errDetail}`);
658
- this.debugLog("error", errDetail);
659
- this.errorCount++;
660
- }
661
- } catch (fetchError) {
662
- const sentViaBeacon = typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && navigator.sendBeacon(url, payload);
663
- const errMsg = fetchError instanceof Error ? fetchError.message : String(fetchError);
664
- if (!sentViaBeacon) {
665
- console.error(
666
- `[Featurely] Failed to send analytics event "${event.eventName}":`,
667
- fetchError
668
- );
669
- this.debugLog(
670
- "error",
671
- `[analytics] send failed for "${event.eventName}": ${errMsg}`
672
- );
673
- this.errorCount++;
674
- } else {
675
- this.debugLog(
676
- "warn",
677
- `[analytics] fetch failed for "${event.eventName}" (${errMsg}), delivered via sendBeacon`
678
- );
679
- }
680
751
  }
681
752
  }
682
753
  }
@@ -1296,6 +1367,69 @@ var _SiteManager = class _SiteManager {
1296
1367
  }
1297
1368
  return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
1298
1369
  }
1370
+ /**
1371
+ * Compare two semver strings. Returns -1 if a < b, 0 if equal, 1 if a > b.
1372
+ * Handles "major.minor.patch" format.
1373
+ */
1374
+ compareVersions(a, b) {
1375
+ const parse = (v) => v.split(".").map((n) => parseInt(n, 10) || 0);
1376
+ const [aMaj, aMin, aPat] = parse(a);
1377
+ const [bMaj, bMin, bPat] = parse(b);
1378
+ if (aMaj !== bMaj) return aMaj < bMaj ? -1 : 1;
1379
+ if (aMin !== bMin) return aMin < bMin ? -1 : 1;
1380
+ if (aPat !== bPat) return aPat < bPat ? -1 : 1;
1381
+ return 0;
1382
+ }
1383
+ /**
1384
+ * Show a dismissible toast notification prompting developers to update the SDK.
1385
+ * This only appears in browser DevTools / on the page when the SDK is outdated.
1386
+ */
1387
+ showSdkUpdateToast(currentVersion, minimumVersion, latestVersion, updateCommand, changelog) {
1388
+ if (typeof document === "undefined") return;
1389
+ const existing = document.getElementById("ft-sdk-update-toast");
1390
+ if (existing) existing.remove();
1391
+ const toast = document.createElement("div");
1392
+ toast.id = "ft-sdk-update-toast";
1393
+ Object.assign(toast.style, {
1394
+ position: "fixed",
1395
+ bottom: "16px",
1396
+ right: "16px",
1397
+ zIndex: "2147483647",
1398
+ background: "#1c1917",
1399
+ color: "#fafaf9",
1400
+ borderLeft: "4px solid #f59e0b",
1401
+ borderRadius: "8px",
1402
+ padding: "14px 16px",
1403
+ maxWidth: "360px",
1404
+ width: "calc(100vw - 32px)",
1405
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
1406
+ fontSize: "13px",
1407
+ boxShadow: "0 4px 32px rgba(0,0,0,0.4)",
1408
+ lineHeight: "1.5",
1409
+ fontWeight: "400"
1410
+ });
1411
+ const changelogHtml = changelog ? `<p style="margin:6px 0 0;color:#a8a29e;font-size:12px;">${changelog}</p>` : "";
1412
+ toast.innerHTML = `
1413
+ <button id="ft-sdk-update-close" style="position:absolute;top:10px;right:10px;background:none;border:none;cursor:pointer;color:#78716c;font-size:16px;line-height:1;padding:2px 4px;" aria-label="Dismiss">\xD7</button>
1414
+ <p style="margin:0 20px 0 0;font-weight:600;color:#fbbf24;">\u{1F4E6} SDK update required</p>
1415
+ <p style="margin:6px 0 0;color:#a8a29e;font-size:12px;">
1416
+ <strong style="color:#fafaf9;">featurely-site-manager</strong>
1417
+ v${currentVersion} is below the minimum v${minimumVersion} (latest: v${latestVersion})
1418
+ </p>
1419
+ ${changelogHtml}
1420
+ <p style="margin:8px 0 0;">
1421
+ <code style="background:#292524;border-radius:4px;padding:3px 7px;font-size:11px;color:#d6d3d1;font-family:monospace;word-break:break-all;">${updateCommand}</code>
1422
+ </p>
1423
+ `;
1424
+ document.body.appendChild(toast);
1425
+ const closeBtn = document.getElementById("ft-sdk-update-close");
1426
+ if (closeBtn) {
1427
+ closeBtn.addEventListener("click", () => toast.remove());
1428
+ }
1429
+ setTimeout(() => {
1430
+ if (toast.parentNode) toast.remove();
1431
+ }, 6e4);
1432
+ }
1299
1433
  // ============================================================================
1300
1434
  // Version Checking
1301
1435
  // ============================================================================
@@ -2190,6 +2324,7 @@ _SiteManager.MAX_CONSECUTIVE_FAILURES = 5;
2190
2324
  var SiteManager = _SiteManager;
2191
2325
  var index_default = SiteManager;
2192
2326
  export {
2327
+ SDK_VERSION,
2193
2328
  SiteManager,
2194
2329
  index_default as default
2195
2330
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "featurely-site-manager",
3
- "version": "1.1.27",
3
+ "version": "1.1.29",
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",