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 +58 -2
- package/dist/index.d.mts +32 -1
- package/dist/index.d.ts +32 -1
- package/dist/index.js +190 -54
- package/dist/index.mjs +189 -54
- package/package.json +1 -1
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,
|
|
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
|
-
##
|
|
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]
|
|
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
|
-
|
|
661
|
-
|
|
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
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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
|
-
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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]
|
|
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
|
-
|
|
626
|
-
|
|
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
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
-
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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.
|
|
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",
|