featurely-site-manager 1.2.0 → 1.3.1
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/CHANGELOG.md +17 -0
- package/README.md +46 -46
- package/dist/index.d.mts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +118 -33
- package/dist/index.mjs +118 -33
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.3.0] - 2026-04-28
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Beta Program Support**: Feature flags now support beta-only targeting
|
|
13
|
+
- `betaUsersOnly` field on feature flags - restrict features to enrolled beta users
|
|
14
|
+
- `betaUserEmails` array in site config - list of users enrolled in beta program
|
|
15
|
+
- Beta enrollment status is automatically evaluated in `isFeatureEnabled()`
|
|
16
|
+
- Percentage rollouts apply within the beta user group when `betaUsersOnly` is true
|
|
17
|
+
- Users can enroll/unenroll via project settings in the Featurely dashboard
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Feature flag evaluation now checks beta enrollment before other targeting rules
|
|
22
|
+
- Non-beta users will never see features marked with `betaUsersOnly: true`
|
|
23
|
+
- SiteConfig interface now includes `betaUserEmails` field
|
|
24
|
+
|
|
8
25
|
## [1.2.0] - 2026-04-21
|
|
9
26
|
|
|
10
27
|
### Added
|
package/README.md
CHANGED
|
@@ -30,42 +30,42 @@ if (manager.isFeatureEnabled("new-checkout")) {
|
|
|
30
30
|
|
|
31
31
|
### `SiteManagerConfig`
|
|
32
32
|
|
|
33
|
-
| Option
|
|
34
|
-
|
|
|
35
|
-
| `apiKey`
|
|
36
|
-
| `projectId`
|
|
37
|
-
| `apiUrl`
|
|
38
|
-
| `environment`
|
|
39
|
-
| `pollInterval`
|
|
40
|
-
| `userEmail`
|
|
41
|
-
| `userId`
|
|
42
|
-
| `customAttributes`
|
|
43
|
-
| `bootstrapFlags`
|
|
44
|
-
| `bypassCheck`
|
|
45
|
-
| `enableAnalytics`
|
|
46
|
-
| `analyticsFlushInterval`
|
|
47
|
-
| `appVersion`
|
|
48
|
-
| `enableVersionCheck`
|
|
49
|
-
| `versionCheckInterval`
|
|
50
|
-
| `platform`
|
|
51
|
-
| `updateRules`
|
|
52
|
-
| `autoInjectBanners`
|
|
53
|
-
| `autoCaptureClicks`
|
|
54
|
-
| `enableHeatmaps`
|
|
55
|
-
| `enableRageClickDetection`
|
|
56
|
-
| `enableScrollTracking`
|
|
57
|
-
| `enablePerformanceTracking` | `boolean`
|
|
58
|
-
| `heatmapSampleRate`
|
|
59
|
-
| `debugMode`
|
|
60
|
-
| `debugSecret`
|
|
61
|
-
| `onMaintenanceEnabled`
|
|
62
|
-
| `onMaintenanceDisabled`
|
|
63
|
-
| `onMessageReceived`
|
|
64
|
-
| `onMessageDismissed`
|
|
65
|
-
| `onFeatureFlagsUpdated`
|
|
66
|
-
| `onUpdateAvailable`
|
|
67
|
-
| `onUpdateRequired`
|
|
68
|
-
| `onError`
|
|
33
|
+
| Option | Type | Default | Description |
|
|
34
|
+
| --------------------------- | --------------------------------------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
35
|
+
| `apiKey` | `string` | required | API key with `public:read` permission |
|
|
36
|
+
| `projectId` | `string` | required | Your Featurely project ID |
|
|
37
|
+
| `apiUrl` | `string` | `"https://www.featurely.no"` | Custom API base URL |
|
|
38
|
+
| `environment` | `string` | — | Hostname override or explicit environment slug (e.g. `process.env.NEXT_PUBLIC_ENV`). Use server-side where `window` is unavailable |
|
|
39
|
+
| `pollInterval` | `number` | `60000` | Config refresh interval in ms |
|
|
40
|
+
| `userEmail` | `string` | — | User email for maintenance whitelist checks |
|
|
41
|
+
| `userId` | `string` | — | User ID for flag bucketing and analytics |
|
|
42
|
+
| `customAttributes` | `Record<string, string \| number \| boolean>` | — | Custom targeting attributes for feature flag rules (e.g. `{ plan: "pro" }`) |
|
|
43
|
+
| `bootstrapFlags` | `Record<string, boolean>` | — | Pre-seed flag values from the server (SSR) to avoid flash on initial render |
|
|
44
|
+
| `bypassCheck` | `() => boolean` | — | Custom maintenance bypass logic |
|
|
45
|
+
| `enableAnalytics` | `boolean` | `true` | Track page views, web vitals, and custom events |
|
|
46
|
+
| `analyticsFlushInterval` | `number` | `60000` | Analytics send interval in ms |
|
|
47
|
+
| `appVersion` | `string` | — | Current app version for version checking |
|
|
48
|
+
| `enableVersionCheck` | `boolean` | `false` | Poll for app version updates |
|
|
49
|
+
| `versionCheckInterval` | `number` | `3600000` | Version check interval in ms |
|
|
50
|
+
| `platform` | `string` | — | Platform identifier for version-check endpoint (`"web" \| "ios" \| "android" \| "electron" \| "tauri"`) |
|
|
51
|
+
| `updateRules` | `VersionUpdateRules` | — | Override server classification per semver change type |
|
|
52
|
+
| `autoInjectBanners` | `boolean` | `true` | Inject status message banners directly into the DOM. Set `false` for custom banner UI |
|
|
53
|
+
| `autoCaptureClicks` | `boolean` | `false` | Track clicks on elements with `data-featurely-click` attribute |
|
|
54
|
+
| `enableHeatmaps` | `boolean` | `false` | Track click coordinates for heatmap visualization |
|
|
55
|
+
| `enableRageClickDetection` | `boolean` | `false` | Detect frustration patterns (5+ clicks in same area) |
|
|
56
|
+
| `enableScrollTracking` | `boolean` | `false` | Track scroll depth at 25%, 50%, 75%, 90%, 100% milestones |
|
|
57
|
+
| `enablePerformanceTracking` | `boolean` | `false` | Monitor resource timing and long tasks |
|
|
58
|
+
| `heatmapSampleRate` | `number` | `10` | Percentage of clicks to sample for heatmaps (0-100) |
|
|
59
|
+
| `debugMode` | `boolean` | `false` | Show floating debug overlay |
|
|
60
|
+
| `debugSecret` | `string` | — | Enable overlay in production via `?ft_debug=<secret>` URL param |
|
|
61
|
+
| `onMaintenanceEnabled` | `(config: MaintenanceConfig) => void` | — | Called when maintenance mode activates |
|
|
62
|
+
| `onMaintenanceDisabled` | `() => void` | — | Called when maintenance mode deactivates |
|
|
63
|
+
| `onMessageReceived` | `(message: StatusMessage) => void` | — | Called when a banner is shown (fires even with auto-injection) |
|
|
64
|
+
| `onMessageDismissed` | `(id: string) => void` | — | Called when a banner is dismissed |
|
|
65
|
+
| `onFeatureFlagsUpdated` | `(flags: FeatureFlag[]) => void` | — | Called when flag config changes |
|
|
66
|
+
| `onUpdateAvailable` | `(info: VersionCheckResponse) => void` | — | Called when a newer app version is available |
|
|
67
|
+
| `onUpdateRequired` | `(info: VersionCheckResponse) => void` | — | Called when an update is mandatory |
|
|
68
|
+
| `onError` | `(error: Error) => void` | — | Called on SDK-internal errors |
|
|
69
69
|
|
|
70
70
|
### `VersionUpdateRules`
|
|
71
71
|
|
|
@@ -217,24 +217,24 @@ const manager = new SiteManager({
|
|
|
217
217
|
apiKey: process.env.NEXT_PUBLIC_FEATURELY_API_KEY!,
|
|
218
218
|
projectId: process.env.NEXT_PUBLIC_PROJECT_ID!,
|
|
219
219
|
enableAnalytics: true,
|
|
220
|
-
enableHeatmaps: true,
|
|
221
|
-
enableRageClickDetection: true,
|
|
222
|
-
enableScrollTracking: true,
|
|
223
|
-
enablePerformanceTracking: true,
|
|
224
|
-
heatmapSampleRate: 10,
|
|
220
|
+
enableHeatmaps: true, // Click heatmaps
|
|
221
|
+
enableRageClickDetection: true, // Frustration detection
|
|
222
|
+
enableScrollTracking: true, // Scroll depth events
|
|
223
|
+
enablePerformanceTracking: true, // Resource timing & long tasks
|
|
224
|
+
heatmapSampleRate: 10, // Sample 10% of clicks
|
|
225
225
|
});
|
|
226
226
|
await manager.init();
|
|
227
227
|
|
|
228
228
|
// Track revenue
|
|
229
|
-
manager.trackRevenue(
|
|
230
|
-
productId:
|
|
229
|
+
manager.trackRevenue("purchase", 4999, "USD", {
|
|
230
|
+
productId: "pro-plan",
|
|
231
231
|
quantity: 1,
|
|
232
232
|
});
|
|
233
233
|
|
|
234
234
|
// Track custom events
|
|
235
|
-
manager.trackEvent(
|
|
236
|
-
plan:
|
|
237
|
-
source:
|
|
235
|
+
manager.trackEvent("signup_completed", {
|
|
236
|
+
plan: "pro",
|
|
237
|
+
source: "landing-page",
|
|
238
238
|
});
|
|
239
239
|
```
|
|
240
240
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
declare const SDK_VERSION = "1.1
|
|
1
|
+
declare const SDK_VERSION = "1.3.1";
|
|
2
2
|
type MessageType = "info" | "warning" | "error" | "success";
|
|
3
3
|
type MessagePosition = "top" | "bottom";
|
|
4
4
|
type MessageStyle = "banner" | "toast";
|
|
@@ -30,6 +30,8 @@ interface FeatureFlag {
|
|
|
30
30
|
rolloutPercentage?: number;
|
|
31
31
|
targetEmails?: string[];
|
|
32
32
|
excludeEmails?: string[];
|
|
33
|
+
betaUsersOnly?: boolean;
|
|
34
|
+
availableOnLocalhost?: boolean;
|
|
33
35
|
variants?: {
|
|
34
36
|
key: string;
|
|
35
37
|
name: string;
|
|
@@ -101,6 +103,7 @@ interface SiteConfig {
|
|
|
101
103
|
maintenance: MaintenanceConfig;
|
|
102
104
|
messages: StatusMessage[];
|
|
103
105
|
featureFlags: FeatureFlag[];
|
|
106
|
+
betaUserEmails?: string[];
|
|
104
107
|
lastUpdated: string;
|
|
105
108
|
analyticsConfig?: AnalyticsConfig | null;
|
|
106
109
|
sdkVersions?: SdkVersionRequirement[];
|
|
@@ -245,6 +248,7 @@ declare class SiteManager {
|
|
|
245
248
|
private matchHostnamePattern;
|
|
246
249
|
private evaluateFeatureFlag;
|
|
247
250
|
private getUserBucket;
|
|
251
|
+
private isLocalhost;
|
|
248
252
|
private simpleHash;
|
|
249
253
|
private getAnonymousId;
|
|
250
254
|
private getOrCreateVisitorId;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
declare const SDK_VERSION = "1.1
|
|
1
|
+
declare const SDK_VERSION = "1.3.1";
|
|
2
2
|
type MessageType = "info" | "warning" | "error" | "success";
|
|
3
3
|
type MessagePosition = "top" | "bottom";
|
|
4
4
|
type MessageStyle = "banner" | "toast";
|
|
@@ -30,6 +30,8 @@ interface FeatureFlag {
|
|
|
30
30
|
rolloutPercentage?: number;
|
|
31
31
|
targetEmails?: string[];
|
|
32
32
|
excludeEmails?: string[];
|
|
33
|
+
betaUsersOnly?: boolean;
|
|
34
|
+
availableOnLocalhost?: boolean;
|
|
33
35
|
variants?: {
|
|
34
36
|
key: string;
|
|
35
37
|
name: string;
|
|
@@ -101,6 +103,7 @@ interface SiteConfig {
|
|
|
101
103
|
maintenance: MaintenanceConfig;
|
|
102
104
|
messages: StatusMessage[];
|
|
103
105
|
featureFlags: FeatureFlag[];
|
|
106
|
+
betaUserEmails?: string[];
|
|
104
107
|
lastUpdated: string;
|
|
105
108
|
analyticsConfig?: AnalyticsConfig | null;
|
|
106
109
|
sdkVersions?: SdkVersionRequirement[];
|
|
@@ -245,6 +248,7 @@ declare class SiteManager {
|
|
|
245
248
|
private matchHostnamePattern;
|
|
246
249
|
private evaluateFeatureFlag;
|
|
247
250
|
private getUserBucket;
|
|
251
|
+
private isLocalhost;
|
|
248
252
|
private simpleHash;
|
|
249
253
|
private getAnonymousId;
|
|
250
254
|
private getOrCreateVisitorId;
|
package/dist/index.js
CHANGED
|
@@ -36,7 +36,7 @@ __export(index_exports, {
|
|
|
36
36
|
});
|
|
37
37
|
module.exports = __toCommonJS(index_exports);
|
|
38
38
|
var import_dompurify = __toESM(require("dompurify"));
|
|
39
|
-
var SDK_VERSION = "1.1
|
|
39
|
+
var SDK_VERSION = "1.3.1";
|
|
40
40
|
var _SiteManager = class _SiteManager {
|
|
41
41
|
constructor(config) {
|
|
42
42
|
this.siteConfig = null;
|
|
@@ -153,9 +153,6 @@ var _SiteManager = class _SiteManager {
|
|
|
153
153
|
*/
|
|
154
154
|
async init() {
|
|
155
155
|
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
156
|
-
console.warn(
|
|
157
|
-
"Featurely Site Manager: Can only be initialized in a browser environment"
|
|
158
|
-
);
|
|
159
156
|
return;
|
|
160
157
|
}
|
|
161
158
|
if (this.config.debugMode) {
|
|
@@ -259,6 +256,7 @@ var _SiteManager = class _SiteManager {
|
|
|
259
256
|
return defaultValue;
|
|
260
257
|
}
|
|
261
258
|
const flag = this.siteConfig.featureFlags.find((f) => f.key === flagKey);
|
|
259
|
+
this.debugLog("info", `[flag-eval] Checking "${flagKey}" - found: ${!!flag}, enabled: ${(flag == null ? void 0 : flag.enabled) || false}`);
|
|
262
260
|
if (!flag || !flag.enabled) {
|
|
263
261
|
return defaultValue;
|
|
264
262
|
}
|
|
@@ -538,15 +536,15 @@ var _SiteManager = class _SiteManager {
|
|
|
538
536
|
const errorData = await response.json().catch(() => ({}));
|
|
539
537
|
const message = errorData.error || response.statusText;
|
|
540
538
|
if (response.status === 401) {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
539
|
+
this.debugLog(
|
|
540
|
+
"error",
|
|
541
|
+
`[config] Invalid or missing API key. ${message}. Check your API key at https://www.featurely.no/dashboard/settings`
|
|
544
542
|
);
|
|
545
543
|
this.stopPolling();
|
|
546
544
|
} else if (response.status === 403) {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
545
|
+
this.debugLog(
|
|
546
|
+
"error",
|
|
547
|
+
`[config] Permission denied. ${message}. Ensure your API key has the required permissions at https://www.featurely.no/dashboard/settings`
|
|
550
548
|
);
|
|
551
549
|
this.stopPolling();
|
|
552
550
|
}
|
|
@@ -698,23 +696,17 @@ var _SiteManager = class _SiteManager {
|
|
|
698
696
|
if (this.consecutiveFetchFailures <= _SiteManager.MAX_CONSECUTIVE_FAILURES) {
|
|
699
697
|
const isNetworkError = error instanceof TypeError && (error.message.includes("NetworkError") || error.message.includes("Failed to fetch") || error.message.includes("fetch"));
|
|
700
698
|
if (isNetworkError) {
|
|
701
|
-
|
|
702
|
-
\u2192 If your site has a Content-Security-Policy, add 'https://www.featurely.no' to the connect-src directive.
|
|
703
|
-
Example: connect-src 'self' https://www.featurely.no ...`;
|
|
704
|
-
console.error(cspMsg);
|
|
705
|
-
this.debugLog("error", "[config] possible CSP block \u2014 check connect-src");
|
|
699
|
+
this.debugLog("error", "[config] Network error - request blocked. Check Content-Security-Policy connect-src directive includes https://www.featurely.no");
|
|
706
700
|
} else {
|
|
707
|
-
|
|
701
|
+
this.debugLog("error", `[config] Failed to fetch configuration: ${error instanceof Error ? error.message : String(error)}`);
|
|
708
702
|
}
|
|
709
703
|
if (this.config.onError && error instanceof Error) {
|
|
710
704
|
this.config.onError(error);
|
|
711
705
|
}
|
|
712
706
|
} else if (this.consecutiveFetchFailures === _SiteManager.MAX_CONSECUTIVE_FAILURES + 1) {
|
|
713
|
-
const silenceMsg = `Featurely Site Manager: Silencing repeated fetch errors after ${_SiteManager.MAX_CONSECUTIVE_FAILURES} failures. Check your apiUrl and Content-Security-Policy configuration.`;
|
|
714
|
-
console.error(silenceMsg);
|
|
715
707
|
this.debugLog(
|
|
716
708
|
"error",
|
|
717
|
-
`[config]
|
|
709
|
+
`[config] Silencing repeated fetch errors after ${_SiteManager.MAX_CONSECUTIVE_FAILURES} consecutive failures. Check your apiUrl and Content-Security-Policy configuration.`
|
|
718
710
|
);
|
|
719
711
|
}
|
|
720
712
|
}
|
|
@@ -797,7 +789,6 @@ var _SiteManager = class _SiteManager {
|
|
|
797
789
|
if (!res.ok) {
|
|
798
790
|
const body = await res.text().catch(() => "(unreadable)");
|
|
799
791
|
const errDetail = `[analytics] batch rejected: ${res.status} ${res.statusText} \u2014 ${body}`;
|
|
800
|
-
console.error(`[Featurely] ${errDetail}`);
|
|
801
792
|
this.debugLog("error", errDetail);
|
|
802
793
|
this.errorCount++;
|
|
803
794
|
}
|
|
@@ -805,7 +796,6 @@ var _SiteManager = class _SiteManager {
|
|
|
805
796
|
const sentViaBeacon = typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && navigator.sendBeacon(url, payload);
|
|
806
797
|
const errMsg = fetchError instanceof Error ? fetchError.message : String(fetchError);
|
|
807
798
|
if (!sentViaBeacon) {
|
|
808
|
-
console.error(`[Featurely] Failed to send analytics batch:`, fetchError);
|
|
809
799
|
this.debugLog("error", `[analytics] batch send failed: ${errMsg}`);
|
|
810
800
|
this.errorCount++;
|
|
811
801
|
} else {
|
|
@@ -922,12 +912,12 @@ var _SiteManager = class _SiteManager {
|
|
|
922
912
|
this.debugOverlayEl = el;
|
|
923
913
|
this.setupGlobalErrorCapture();
|
|
924
914
|
this.debugLog("info", `[site-manager] debug overlay initialized`);
|
|
925
|
-
this.debugLog("info", `[site-manager]
|
|
915
|
+
this.debugLog("info", `[site-manager] v${SDK_VERSION} | project: ${this.config.projectId}`);
|
|
926
916
|
this.renderDebugOverlay();
|
|
927
917
|
this.debugRefreshId = setInterval(() => this.renderDebugOverlay(), 1500);
|
|
928
918
|
}
|
|
929
919
|
renderDebugOverlay() {
|
|
930
|
-
var _a, _b, _c, _d, _e;
|
|
920
|
+
var _a, _b, _c, _d, _e, _f;
|
|
931
921
|
const el = this.debugOverlayEl;
|
|
932
922
|
if (!el) return;
|
|
933
923
|
const ACCENT = "#6366f1";
|
|
@@ -954,7 +944,7 @@ var _SiteManager = class _SiteManager {
|
|
|
954
944
|
const flagCount = (_c = (_b = sc == null ? void 0 : sc.featureFlags) == null ? void 0 : _b.length) != null ? _c : 0;
|
|
955
945
|
const msgCount = (_e = (_d = sc == null ? void 0 : sc.messages) == null ? void 0 : _d.length) != null ? _e : 0;
|
|
956
946
|
content = `
|
|
957
|
-
${row("SDK Version",
|
|
947
|
+
${row("SDK Version", SDK_VERSION)}
|
|
958
948
|
${row("Project", cfg.projectId)}
|
|
959
949
|
${row("Hostname", typeof window !== "undefined" ? window.location.hostname : "\u2014")}
|
|
960
950
|
${row("API URL", cfg.apiUrl)}
|
|
@@ -1012,6 +1002,64 @@ var _SiteManager = class _SiteManager {
|
|
|
1012
1002
|
(e) => `<div style="padding:2px 0;display:flex;justify-content:space-between;border-bottom:1px solid rgba(255,255,255,0.03)"><span style="color:#a5b4fc">${e.name}</span><span style="color:${MUTED}">${e.ts}</span></div>`
|
|
1013
1003
|
).join("");
|
|
1014
1004
|
}
|
|
1005
|
+
} else if (tab === "flags") {
|
|
1006
|
+
const flags = (sc == null ? void 0 : sc.featureFlags) || [];
|
|
1007
|
+
if (flags.length === 0) {
|
|
1008
|
+
content = `<div style="color:${MUTED};padding:16px;text-align:center">No feature flags configured</div>`;
|
|
1009
|
+
} else {
|
|
1010
|
+
const flagCards = flags.map((flag) => {
|
|
1011
|
+
var _a2, _b2;
|
|
1012
|
+
const isEnabled = this.isFeatureEnabled(flag.key);
|
|
1013
|
+
const statusColor = isEnabled ? GREEN : MUTED;
|
|
1014
|
+
const statusIcon = isEnabled ? "\u2713" : "\u2717";
|
|
1015
|
+
const details = [];
|
|
1016
|
+
if (flag.rolloutPercentage !== void 0 && flag.rolloutPercentage < 100) {
|
|
1017
|
+
const bucket = this.getUserBucket(flag.key);
|
|
1018
|
+
details.push(`Rollout: ${flag.rolloutPercentage}% (bucket: ${bucket.toFixed(0)})`);
|
|
1019
|
+
}
|
|
1020
|
+
if (flag.betaUsersOnly) {
|
|
1021
|
+
const isBeta = (_b2 = (_a2 = this.siteConfig) == null ? void 0 : _a2.betaUserEmails) == null ? void 0 : _b2.includes(this.config.userEmail || "");
|
|
1022
|
+
details.push(`Beta only: ${isBeta ? "\u2713 enrolled" : "\u2717 not enrolled"}`);
|
|
1023
|
+
}
|
|
1024
|
+
if (flag.availableOnLocalhost) {
|
|
1025
|
+
const isLocal = typeof window !== "undefined" && (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1");
|
|
1026
|
+
details.push(`Localhost: ${isLocal ? "\u2713" : "\u2717"}`);
|
|
1027
|
+
}
|
|
1028
|
+
if (flag.targetEmails && flag.targetEmails.length > 0) {
|
|
1029
|
+
const isTargeted = flag.targetEmails.includes(this.config.userEmail || "");
|
|
1030
|
+
details.push(`Email targeted: ${isTargeted ? "\u2713" : "\u2717"}`);
|
|
1031
|
+
}
|
|
1032
|
+
if (flag.excludeEmails && flag.excludeEmails.length > 0) {
|
|
1033
|
+
const isExcluded = flag.excludeEmails.includes(this.config.userEmail || "");
|
|
1034
|
+
details.push(`Excluded: ${isExcluded ? "\u2713" : "\u2717"}`);
|
|
1035
|
+
}
|
|
1036
|
+
if (flag.targetAttributes && flag.targetAttributes.length > 0) {
|
|
1037
|
+
details.push(`Custom attributes: ${flag.targetAttributes.length} rule(s)`);
|
|
1038
|
+
}
|
|
1039
|
+
if (flag.variants && flag.variants.length > 0) {
|
|
1040
|
+
const variant = this.getFeatureVariant(flag.key);
|
|
1041
|
+
details.push(`Variant: ${variant || flag.defaultVariant || "default"}`);
|
|
1042
|
+
}
|
|
1043
|
+
const detailsHtml = details.length > 0 ? `<div style="font-size:9px;color:${MUTED};margin-top:3px;line-height:1.4">${details.join(" \u2022 ")}</div>` : "";
|
|
1044
|
+
return `
|
|
1045
|
+
<div style="padding:6px 8px;margin:4px 0;background:rgba(255,255,255,0.03);border-radius:6px;border-left:3px solid ${statusColor}">
|
|
1046
|
+
<div style="display:flex;justify-content:space-between;align-items:center">
|
|
1047
|
+
<div style="flex:1">
|
|
1048
|
+
<div style="display:flex;align-items:center;gap:6px">
|
|
1049
|
+
<span style="color:${statusColor};font-weight:600;font-size:10px">${statusIcon}</span>
|
|
1050
|
+
<span style="color:${TEXT};font-weight:600;font-family:monospace;font-size:10px">${flag.key}</span>
|
|
1051
|
+
${!flag.enabled ? badge("DISABLED", MUTED) : ""}
|
|
1052
|
+
</div>
|
|
1053
|
+
${flag.name ? `<div style="color:${MUTED};font-size:9px;margin-top:2px">${flag.name}</div>` : ""}
|
|
1054
|
+
${detailsHtml}
|
|
1055
|
+
</div>
|
|
1056
|
+
</div>
|
|
1057
|
+
</div>
|
|
1058
|
+
`;
|
|
1059
|
+
}).join("");
|
|
1060
|
+
const summary = `<div style="display:flex;justify-content:space-between;margin-bottom:8px;padding:4px 0;border-bottom:1px solid ${BORDER}"><span style="color:${MUTED};font-size:9px">TOTAL: ${flags.length}</span><span style="color:${MUTED};font-size:9px">USER: ${this.config.userEmail || this.config.userId || "anonymous"}</span></div>`;
|
|
1061
|
+
content = summary + `<div style="max-height:250px;overflow-y:auto">${flagCards}</div>`;
|
|
1062
|
+
}
|
|
1015
1063
|
} else if (tab === "test") {
|
|
1016
1064
|
const btnStyle = (color) => `background:${color};border:none;color:#fff;cursor:pointer;font-size:10px;font-family:inherit;padding:4px 10px;border-radius:4px;transition:opacity 0.1s`;
|
|
1017
1065
|
const sectionLabel = (text) => `<div style="color:${MUTED};font-size:9px;text-transform:uppercase;letter-spacing:0.08em;margin-bottom:6px;margin-top:10px">${text}</div>`;
|
|
@@ -1053,7 +1101,7 @@ var _SiteManager = class _SiteManager {
|
|
|
1053
1101
|
<div style="background:rgba(255,255,255,0.04);border-bottom:1px solid ${BORDER};padding:7px 10px;display:flex;align-items:center;gap:6px;cursor:pointer" id="__ft_dbg_hdr__">
|
|
1054
1102
|
<span style="color:${ACCENT};font-weight:700;font-size:10px;letter-spacing:0.05em">⬡ FEATURELY DEBUG</span>
|
|
1055
1103
|
<span style="flex:1"></span>
|
|
1056
|
-
${minimized ? "" : `<div style="display:flex;gap:4px;flex-wrap:wrap">${tabBtn("sdk", "SDK")}${tabBtn("logs", "Logs", this.errorCount)}${tabBtn("network", "Network")}${tabBtn("events", "Events")}${tabBtn("test", "Test")}</div>`}
|
|
1104
|
+
${minimized ? "" : `<div style="display:flex;gap:4px;flex-wrap:wrap">${tabBtn("sdk", "SDK")}${tabBtn("flags", "Flags", ((_f = sc == null ? void 0 : sc.featureFlags) == null ? void 0 : _f.length) || 0)}${tabBtn("logs", "Logs", this.errorCount)}${tabBtn("network", "Network")}${tabBtn("events", "Events")}${tabBtn("test", "Test")}</div>`}
|
|
1057
1105
|
<button id="__ft_dbg_min__" style="background:none;border:none;color:${MUTED};cursor:pointer;font-size:14px;line-height:1;padding:0 2px" title="${minimized ? "Expand" : "Minimize"}">${minimized ? "⬆" : "⬇"}</button>
|
|
1058
1106
|
<button id="__ft_dbg_cls__" style="background:none;border:none;color:${MUTED};cursor:pointer;font-size:14px;line-height:1;padding:0 2px" title="Close">×</button>
|
|
1059
1107
|
</div>
|
|
@@ -1694,7 +1742,7 @@ var _SiteManager = class _SiteManager {
|
|
|
1694
1742
|
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
1695
1743
|
const versionToCheck = currentVersion || this.config.appVersion;
|
|
1696
1744
|
if (!versionToCheck) {
|
|
1697
|
-
|
|
1745
|
+
this.debugLog("warn", "[version-check] appVersion not provided");
|
|
1698
1746
|
return null;
|
|
1699
1747
|
}
|
|
1700
1748
|
try {
|
|
@@ -1773,7 +1821,7 @@ var _SiteManager = class _SiteManager {
|
|
|
1773
1821
|
}
|
|
1774
1822
|
return versionInfo;
|
|
1775
1823
|
} catch (error) {
|
|
1776
|
-
|
|
1824
|
+
this.debugLog("error", `[version-check] ${error instanceof Error ? error.message : String(error)}`);
|
|
1777
1825
|
if (this.config.onError) {
|
|
1778
1826
|
this.config.onError(
|
|
1779
1827
|
error instanceof Error ? error : new Error("Failed to check version")
|
|
@@ -1898,16 +1946,34 @@ var _SiteManager = class _SiteManager {
|
|
|
1898
1946
|
* Evaluate if a feature flag should be enabled for the current user
|
|
1899
1947
|
*/
|
|
1900
1948
|
evaluateFeatureFlag(flag) {
|
|
1901
|
-
var _a;
|
|
1949
|
+
var _a, _b;
|
|
1950
|
+
if (flag.availableOnLocalhost && this.isLocalhost()) {
|
|
1951
|
+
this.debugLog("info", `[flag-eval] "${flag.key}": ENABLED (localhost bypass)`);
|
|
1952
|
+
return true;
|
|
1953
|
+
}
|
|
1902
1954
|
if (!flag.enabled) {
|
|
1955
|
+
this.debugLog("info", `[flag-eval] "${flag.key}": DISABLED (flag.enabled=false)`);
|
|
1903
1956
|
return false;
|
|
1904
1957
|
}
|
|
1905
1958
|
const userEmail = this.config.userEmail;
|
|
1906
1959
|
if (flag.excludeEmails && userEmail && flag.excludeEmails.includes(userEmail)) {
|
|
1960
|
+
this.debugLog("info", `[flag-eval] "${flag.key}": DISABLED (user ${userEmail} excluded)`);
|
|
1907
1961
|
return false;
|
|
1908
1962
|
}
|
|
1963
|
+
if (flag.betaUsersOnly) {
|
|
1964
|
+
if (!userEmail) {
|
|
1965
|
+
this.debugLog("info", `[flag-eval] "${flag.key}": DISABLED (beta-only, no email provided)`);
|
|
1966
|
+
return false;
|
|
1967
|
+
}
|
|
1968
|
+
const betaUsers = ((_a = this.siteConfig) == null ? void 0 : _a.betaUserEmails) || [];
|
|
1969
|
+
if (!betaUsers.includes(userEmail)) {
|
|
1970
|
+
this.debugLog("info", `[flag-eval] "${flag.key}": DISABLED (beta-only, ${userEmail} not in beta list)`);
|
|
1971
|
+
return false;
|
|
1972
|
+
}
|
|
1973
|
+
this.debugLog("info", `[flag-eval] "${flag.key}": beta check passed for ${userEmail}`);
|
|
1974
|
+
}
|
|
1909
1975
|
if (flag.targetAttributes && flag.targetAttributes.length > 0) {
|
|
1910
|
-
const attrs = (
|
|
1976
|
+
const attrs = (_b = this.config.customAttributes) != null ? _b : {};
|
|
1911
1977
|
const allMatch = flag.targetAttributes.every((rule) => {
|
|
1912
1978
|
const actual = attrs[rule.attribute];
|
|
1913
1979
|
if (actual === void 0) return false;
|
|
@@ -1926,18 +1992,27 @@ var _SiteManager = class _SiteManager {
|
|
|
1926
1992
|
return false;
|
|
1927
1993
|
}
|
|
1928
1994
|
});
|
|
1929
|
-
if (!allMatch)
|
|
1995
|
+
if (!allMatch) {
|
|
1996
|
+
this.debugLog("info", `[flag-eval] "${flag.key}": DISABLED (custom attribute rules not matched)`);
|
|
1997
|
+
return false;
|
|
1998
|
+
}
|
|
1999
|
+
this.debugLog("info", `[flag-eval] "${flag.key}": custom attribute rules matched`);
|
|
1930
2000
|
}
|
|
1931
2001
|
if (flag.targetEmails && flag.targetEmails.length > 0) {
|
|
1932
2002
|
if (!userEmail || !flag.targetEmails.includes(userEmail)) {
|
|
2003
|
+
this.debugLog("info", `[flag-eval] "${flag.key}": DISABLED (not in targetEmails)`);
|
|
1933
2004
|
return false;
|
|
1934
2005
|
}
|
|
2006
|
+
this.debugLog("info", `[flag-eval] "${flag.key}": ENABLED (user in targetEmails)`);
|
|
1935
2007
|
return true;
|
|
1936
2008
|
}
|
|
1937
2009
|
if (flag.rolloutPercentage !== void 0 && flag.rolloutPercentage < 100) {
|
|
1938
2010
|
const bucket = this.getUserBucket(flag.key);
|
|
1939
|
-
|
|
2011
|
+
const enabled = bucket < flag.rolloutPercentage;
|
|
2012
|
+
this.debugLog("info", `[flag-eval] "${flag.key}": ${enabled ? "ENABLED" : "DISABLED"} (rollout ${flag.rolloutPercentage}%, bucket ${bucket})`);
|
|
2013
|
+
return enabled;
|
|
1940
2014
|
}
|
|
2015
|
+
this.debugLog("info", `[flag-eval] "${flag.key}": ENABLED (no restrictions)`);
|
|
1941
2016
|
return true;
|
|
1942
2017
|
}
|
|
1943
2018
|
/**
|
|
@@ -1954,6 +2029,16 @@ var _SiteManager = class _SiteManager {
|
|
|
1954
2029
|
this.featureFlagBuckets.set(flagKey, bucket);
|
|
1955
2030
|
return bucket;
|
|
1956
2031
|
}
|
|
2032
|
+
/**
|
|
2033
|
+
* Check if the current environment is localhost
|
|
2034
|
+
*/
|
|
2035
|
+
isLocalhost() {
|
|
2036
|
+
if (typeof window === "undefined") {
|
|
2037
|
+
return false;
|
|
2038
|
+
}
|
|
2039
|
+
const hostname = window.location.hostname;
|
|
2040
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
2041
|
+
}
|
|
1957
2042
|
/**
|
|
1958
2043
|
* Simple hash function for consistent bucketing
|
|
1959
2044
|
*/
|
|
@@ -2164,7 +2249,7 @@ var _SiteManager = class _SiteManager {
|
|
|
2164
2249
|
const regex = new RegExp(escapedPattern);
|
|
2165
2250
|
return regex.test(currentPath);
|
|
2166
2251
|
} catch (e) {
|
|
2167
|
-
|
|
2252
|
+
this.debugLog("warn", `[message] Invalid pattern: ${pattern}`);
|
|
2168
2253
|
return false;
|
|
2169
2254
|
}
|
|
2170
2255
|
});
|
|
@@ -2268,7 +2353,7 @@ var _SiteManager = class _SiteManager {
|
|
|
2268
2353
|
}
|
|
2269
2354
|
handleMessageAction(action, message) {
|
|
2270
2355
|
var _a;
|
|
2271
|
-
|
|
2356
|
+
this.debugLog("info", `[message-action] ${action} on message ${message.id}`);
|
|
2272
2357
|
if ((_a = message.cta) == null ? void 0 : _a.url) {
|
|
2273
2358
|
window.location.href = message.cta.url;
|
|
2274
2359
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import DOMPurify from "dompurify";
|
|
3
|
-
var SDK_VERSION = "1.1
|
|
3
|
+
var SDK_VERSION = "1.3.1";
|
|
4
4
|
var _SiteManager = class _SiteManager {
|
|
5
5
|
constructor(config) {
|
|
6
6
|
this.siteConfig = null;
|
|
@@ -117,9 +117,6 @@ var _SiteManager = class _SiteManager {
|
|
|
117
117
|
*/
|
|
118
118
|
async init() {
|
|
119
119
|
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
120
|
-
console.warn(
|
|
121
|
-
"Featurely Site Manager: Can only be initialized in a browser environment"
|
|
122
|
-
);
|
|
123
120
|
return;
|
|
124
121
|
}
|
|
125
122
|
if (this.config.debugMode) {
|
|
@@ -223,6 +220,7 @@ var _SiteManager = class _SiteManager {
|
|
|
223
220
|
return defaultValue;
|
|
224
221
|
}
|
|
225
222
|
const flag = this.siteConfig.featureFlags.find((f) => f.key === flagKey);
|
|
223
|
+
this.debugLog("info", `[flag-eval] Checking "${flagKey}" - found: ${!!flag}, enabled: ${(flag == null ? void 0 : flag.enabled) || false}`);
|
|
226
224
|
if (!flag || !flag.enabled) {
|
|
227
225
|
return defaultValue;
|
|
228
226
|
}
|
|
@@ -502,15 +500,15 @@ var _SiteManager = class _SiteManager {
|
|
|
502
500
|
const errorData = await response.json().catch(() => ({}));
|
|
503
501
|
const message = errorData.error || response.statusText;
|
|
504
502
|
if (response.status === 401) {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
503
|
+
this.debugLog(
|
|
504
|
+
"error",
|
|
505
|
+
`[config] Invalid or missing API key. ${message}. Check your API key at https://www.featurely.no/dashboard/settings`
|
|
508
506
|
);
|
|
509
507
|
this.stopPolling();
|
|
510
508
|
} else if (response.status === 403) {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
509
|
+
this.debugLog(
|
|
510
|
+
"error",
|
|
511
|
+
`[config] Permission denied. ${message}. Ensure your API key has the required permissions at https://www.featurely.no/dashboard/settings`
|
|
514
512
|
);
|
|
515
513
|
this.stopPolling();
|
|
516
514
|
}
|
|
@@ -662,23 +660,17 @@ var _SiteManager = class _SiteManager {
|
|
|
662
660
|
if (this.consecutiveFetchFailures <= _SiteManager.MAX_CONSECUTIVE_FAILURES) {
|
|
663
661
|
const isNetworkError = error instanceof TypeError && (error.message.includes("NetworkError") || error.message.includes("Failed to fetch") || error.message.includes("fetch"));
|
|
664
662
|
if (isNetworkError) {
|
|
665
|
-
|
|
666
|
-
\u2192 If your site has a Content-Security-Policy, add 'https://www.featurely.no' to the connect-src directive.
|
|
667
|
-
Example: connect-src 'self' https://www.featurely.no ...`;
|
|
668
|
-
console.error(cspMsg);
|
|
669
|
-
this.debugLog("error", "[config] possible CSP block \u2014 check connect-src");
|
|
663
|
+
this.debugLog("error", "[config] Network error - request blocked. Check Content-Security-Policy connect-src directive includes https://www.featurely.no");
|
|
670
664
|
} else {
|
|
671
|
-
|
|
665
|
+
this.debugLog("error", `[config] Failed to fetch configuration: ${error instanceof Error ? error.message : String(error)}`);
|
|
672
666
|
}
|
|
673
667
|
if (this.config.onError && error instanceof Error) {
|
|
674
668
|
this.config.onError(error);
|
|
675
669
|
}
|
|
676
670
|
} else if (this.consecutiveFetchFailures === _SiteManager.MAX_CONSECUTIVE_FAILURES + 1) {
|
|
677
|
-
const silenceMsg = `Featurely Site Manager: Silencing repeated fetch errors after ${_SiteManager.MAX_CONSECUTIVE_FAILURES} failures. Check your apiUrl and Content-Security-Policy configuration.`;
|
|
678
|
-
console.error(silenceMsg);
|
|
679
671
|
this.debugLog(
|
|
680
672
|
"error",
|
|
681
|
-
`[config]
|
|
673
|
+
`[config] Silencing repeated fetch errors after ${_SiteManager.MAX_CONSECUTIVE_FAILURES} consecutive failures. Check your apiUrl and Content-Security-Policy configuration.`
|
|
682
674
|
);
|
|
683
675
|
}
|
|
684
676
|
}
|
|
@@ -761,7 +753,6 @@ var _SiteManager = class _SiteManager {
|
|
|
761
753
|
if (!res.ok) {
|
|
762
754
|
const body = await res.text().catch(() => "(unreadable)");
|
|
763
755
|
const errDetail = `[analytics] batch rejected: ${res.status} ${res.statusText} \u2014 ${body}`;
|
|
764
|
-
console.error(`[Featurely] ${errDetail}`);
|
|
765
756
|
this.debugLog("error", errDetail);
|
|
766
757
|
this.errorCount++;
|
|
767
758
|
}
|
|
@@ -769,7 +760,6 @@ var _SiteManager = class _SiteManager {
|
|
|
769
760
|
const sentViaBeacon = typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && navigator.sendBeacon(url, payload);
|
|
770
761
|
const errMsg = fetchError instanceof Error ? fetchError.message : String(fetchError);
|
|
771
762
|
if (!sentViaBeacon) {
|
|
772
|
-
console.error(`[Featurely] Failed to send analytics batch:`, fetchError);
|
|
773
763
|
this.debugLog("error", `[analytics] batch send failed: ${errMsg}`);
|
|
774
764
|
this.errorCount++;
|
|
775
765
|
} else {
|
|
@@ -886,12 +876,12 @@ var _SiteManager = class _SiteManager {
|
|
|
886
876
|
this.debugOverlayEl = el;
|
|
887
877
|
this.setupGlobalErrorCapture();
|
|
888
878
|
this.debugLog("info", `[site-manager] debug overlay initialized`);
|
|
889
|
-
this.debugLog("info", `[site-manager]
|
|
879
|
+
this.debugLog("info", `[site-manager] v${SDK_VERSION} | project: ${this.config.projectId}`);
|
|
890
880
|
this.renderDebugOverlay();
|
|
891
881
|
this.debugRefreshId = setInterval(() => this.renderDebugOverlay(), 1500);
|
|
892
882
|
}
|
|
893
883
|
renderDebugOverlay() {
|
|
894
|
-
var _a, _b, _c, _d, _e;
|
|
884
|
+
var _a, _b, _c, _d, _e, _f;
|
|
895
885
|
const el = this.debugOverlayEl;
|
|
896
886
|
if (!el) return;
|
|
897
887
|
const ACCENT = "#6366f1";
|
|
@@ -918,7 +908,7 @@ var _SiteManager = class _SiteManager {
|
|
|
918
908
|
const flagCount = (_c = (_b = sc == null ? void 0 : sc.featureFlags) == null ? void 0 : _b.length) != null ? _c : 0;
|
|
919
909
|
const msgCount = (_e = (_d = sc == null ? void 0 : sc.messages) == null ? void 0 : _d.length) != null ? _e : 0;
|
|
920
910
|
content = `
|
|
921
|
-
${row("SDK Version",
|
|
911
|
+
${row("SDK Version", SDK_VERSION)}
|
|
922
912
|
${row("Project", cfg.projectId)}
|
|
923
913
|
${row("Hostname", typeof window !== "undefined" ? window.location.hostname : "\u2014")}
|
|
924
914
|
${row("API URL", cfg.apiUrl)}
|
|
@@ -976,6 +966,64 @@ var _SiteManager = class _SiteManager {
|
|
|
976
966
|
(e) => `<div style="padding:2px 0;display:flex;justify-content:space-between;border-bottom:1px solid rgba(255,255,255,0.03)"><span style="color:#a5b4fc">${e.name}</span><span style="color:${MUTED}">${e.ts}</span></div>`
|
|
977
967
|
).join("");
|
|
978
968
|
}
|
|
969
|
+
} else if (tab === "flags") {
|
|
970
|
+
const flags = (sc == null ? void 0 : sc.featureFlags) || [];
|
|
971
|
+
if (flags.length === 0) {
|
|
972
|
+
content = `<div style="color:${MUTED};padding:16px;text-align:center">No feature flags configured</div>`;
|
|
973
|
+
} else {
|
|
974
|
+
const flagCards = flags.map((flag) => {
|
|
975
|
+
var _a2, _b2;
|
|
976
|
+
const isEnabled = this.isFeatureEnabled(flag.key);
|
|
977
|
+
const statusColor = isEnabled ? GREEN : MUTED;
|
|
978
|
+
const statusIcon = isEnabled ? "\u2713" : "\u2717";
|
|
979
|
+
const details = [];
|
|
980
|
+
if (flag.rolloutPercentage !== void 0 && flag.rolloutPercentage < 100) {
|
|
981
|
+
const bucket = this.getUserBucket(flag.key);
|
|
982
|
+
details.push(`Rollout: ${flag.rolloutPercentage}% (bucket: ${bucket.toFixed(0)})`);
|
|
983
|
+
}
|
|
984
|
+
if (flag.betaUsersOnly) {
|
|
985
|
+
const isBeta = (_b2 = (_a2 = this.siteConfig) == null ? void 0 : _a2.betaUserEmails) == null ? void 0 : _b2.includes(this.config.userEmail || "");
|
|
986
|
+
details.push(`Beta only: ${isBeta ? "\u2713 enrolled" : "\u2717 not enrolled"}`);
|
|
987
|
+
}
|
|
988
|
+
if (flag.availableOnLocalhost) {
|
|
989
|
+
const isLocal = typeof window !== "undefined" && (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1");
|
|
990
|
+
details.push(`Localhost: ${isLocal ? "\u2713" : "\u2717"}`);
|
|
991
|
+
}
|
|
992
|
+
if (flag.targetEmails && flag.targetEmails.length > 0) {
|
|
993
|
+
const isTargeted = flag.targetEmails.includes(this.config.userEmail || "");
|
|
994
|
+
details.push(`Email targeted: ${isTargeted ? "\u2713" : "\u2717"}`);
|
|
995
|
+
}
|
|
996
|
+
if (flag.excludeEmails && flag.excludeEmails.length > 0) {
|
|
997
|
+
const isExcluded = flag.excludeEmails.includes(this.config.userEmail || "");
|
|
998
|
+
details.push(`Excluded: ${isExcluded ? "\u2713" : "\u2717"}`);
|
|
999
|
+
}
|
|
1000
|
+
if (flag.targetAttributes && flag.targetAttributes.length > 0) {
|
|
1001
|
+
details.push(`Custom attributes: ${flag.targetAttributes.length} rule(s)`);
|
|
1002
|
+
}
|
|
1003
|
+
if (flag.variants && flag.variants.length > 0) {
|
|
1004
|
+
const variant = this.getFeatureVariant(flag.key);
|
|
1005
|
+
details.push(`Variant: ${variant || flag.defaultVariant || "default"}`);
|
|
1006
|
+
}
|
|
1007
|
+
const detailsHtml = details.length > 0 ? `<div style="font-size:9px;color:${MUTED};margin-top:3px;line-height:1.4">${details.join(" \u2022 ")}</div>` : "";
|
|
1008
|
+
return `
|
|
1009
|
+
<div style="padding:6px 8px;margin:4px 0;background:rgba(255,255,255,0.03);border-radius:6px;border-left:3px solid ${statusColor}">
|
|
1010
|
+
<div style="display:flex;justify-content:space-between;align-items:center">
|
|
1011
|
+
<div style="flex:1">
|
|
1012
|
+
<div style="display:flex;align-items:center;gap:6px">
|
|
1013
|
+
<span style="color:${statusColor};font-weight:600;font-size:10px">${statusIcon}</span>
|
|
1014
|
+
<span style="color:${TEXT};font-weight:600;font-family:monospace;font-size:10px">${flag.key}</span>
|
|
1015
|
+
${!flag.enabled ? badge("DISABLED", MUTED) : ""}
|
|
1016
|
+
</div>
|
|
1017
|
+
${flag.name ? `<div style="color:${MUTED};font-size:9px;margin-top:2px">${flag.name}</div>` : ""}
|
|
1018
|
+
${detailsHtml}
|
|
1019
|
+
</div>
|
|
1020
|
+
</div>
|
|
1021
|
+
</div>
|
|
1022
|
+
`;
|
|
1023
|
+
}).join("");
|
|
1024
|
+
const summary = `<div style="display:flex;justify-content:space-between;margin-bottom:8px;padding:4px 0;border-bottom:1px solid ${BORDER}"><span style="color:${MUTED};font-size:9px">TOTAL: ${flags.length}</span><span style="color:${MUTED};font-size:9px">USER: ${this.config.userEmail || this.config.userId || "anonymous"}</span></div>`;
|
|
1025
|
+
content = summary + `<div style="max-height:250px;overflow-y:auto">${flagCards}</div>`;
|
|
1026
|
+
}
|
|
979
1027
|
} else if (tab === "test") {
|
|
980
1028
|
const btnStyle = (color) => `background:${color};border:none;color:#fff;cursor:pointer;font-size:10px;font-family:inherit;padding:4px 10px;border-radius:4px;transition:opacity 0.1s`;
|
|
981
1029
|
const sectionLabel = (text) => `<div style="color:${MUTED};font-size:9px;text-transform:uppercase;letter-spacing:0.08em;margin-bottom:6px;margin-top:10px">${text}</div>`;
|
|
@@ -1017,7 +1065,7 @@ var _SiteManager = class _SiteManager {
|
|
|
1017
1065
|
<div style="background:rgba(255,255,255,0.04);border-bottom:1px solid ${BORDER};padding:7px 10px;display:flex;align-items:center;gap:6px;cursor:pointer" id="__ft_dbg_hdr__">
|
|
1018
1066
|
<span style="color:${ACCENT};font-weight:700;font-size:10px;letter-spacing:0.05em">⬡ FEATURELY DEBUG</span>
|
|
1019
1067
|
<span style="flex:1"></span>
|
|
1020
|
-
${minimized ? "" : `<div style="display:flex;gap:4px;flex-wrap:wrap">${tabBtn("sdk", "SDK")}${tabBtn("logs", "Logs", this.errorCount)}${tabBtn("network", "Network")}${tabBtn("events", "Events")}${tabBtn("test", "Test")}</div>`}
|
|
1068
|
+
${minimized ? "" : `<div style="display:flex;gap:4px;flex-wrap:wrap">${tabBtn("sdk", "SDK")}${tabBtn("flags", "Flags", ((_f = sc == null ? void 0 : sc.featureFlags) == null ? void 0 : _f.length) || 0)}${tabBtn("logs", "Logs", this.errorCount)}${tabBtn("network", "Network")}${tabBtn("events", "Events")}${tabBtn("test", "Test")}</div>`}
|
|
1021
1069
|
<button id="__ft_dbg_min__" style="background:none;border:none;color:${MUTED};cursor:pointer;font-size:14px;line-height:1;padding:0 2px" title="${minimized ? "Expand" : "Minimize"}">${minimized ? "⬆" : "⬇"}</button>
|
|
1022
1070
|
<button id="__ft_dbg_cls__" style="background:none;border:none;color:${MUTED};cursor:pointer;font-size:14px;line-height:1;padding:0 2px" title="Close">×</button>
|
|
1023
1071
|
</div>
|
|
@@ -1658,7 +1706,7 @@ var _SiteManager = class _SiteManager {
|
|
|
1658
1706
|
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
1659
1707
|
const versionToCheck = currentVersion || this.config.appVersion;
|
|
1660
1708
|
if (!versionToCheck) {
|
|
1661
|
-
|
|
1709
|
+
this.debugLog("warn", "[version-check] appVersion not provided");
|
|
1662
1710
|
return null;
|
|
1663
1711
|
}
|
|
1664
1712
|
try {
|
|
@@ -1737,7 +1785,7 @@ var _SiteManager = class _SiteManager {
|
|
|
1737
1785
|
}
|
|
1738
1786
|
return versionInfo;
|
|
1739
1787
|
} catch (error) {
|
|
1740
|
-
|
|
1788
|
+
this.debugLog("error", `[version-check] ${error instanceof Error ? error.message : String(error)}`);
|
|
1741
1789
|
if (this.config.onError) {
|
|
1742
1790
|
this.config.onError(
|
|
1743
1791
|
error instanceof Error ? error : new Error("Failed to check version")
|
|
@@ -1862,16 +1910,34 @@ var _SiteManager = class _SiteManager {
|
|
|
1862
1910
|
* Evaluate if a feature flag should be enabled for the current user
|
|
1863
1911
|
*/
|
|
1864
1912
|
evaluateFeatureFlag(flag) {
|
|
1865
|
-
var _a;
|
|
1913
|
+
var _a, _b;
|
|
1914
|
+
if (flag.availableOnLocalhost && this.isLocalhost()) {
|
|
1915
|
+
this.debugLog("info", `[flag-eval] "${flag.key}": ENABLED (localhost bypass)`);
|
|
1916
|
+
return true;
|
|
1917
|
+
}
|
|
1866
1918
|
if (!flag.enabled) {
|
|
1919
|
+
this.debugLog("info", `[flag-eval] "${flag.key}": DISABLED (flag.enabled=false)`);
|
|
1867
1920
|
return false;
|
|
1868
1921
|
}
|
|
1869
1922
|
const userEmail = this.config.userEmail;
|
|
1870
1923
|
if (flag.excludeEmails && userEmail && flag.excludeEmails.includes(userEmail)) {
|
|
1924
|
+
this.debugLog("info", `[flag-eval] "${flag.key}": DISABLED (user ${userEmail} excluded)`);
|
|
1871
1925
|
return false;
|
|
1872
1926
|
}
|
|
1927
|
+
if (flag.betaUsersOnly) {
|
|
1928
|
+
if (!userEmail) {
|
|
1929
|
+
this.debugLog("info", `[flag-eval] "${flag.key}": DISABLED (beta-only, no email provided)`);
|
|
1930
|
+
return false;
|
|
1931
|
+
}
|
|
1932
|
+
const betaUsers = ((_a = this.siteConfig) == null ? void 0 : _a.betaUserEmails) || [];
|
|
1933
|
+
if (!betaUsers.includes(userEmail)) {
|
|
1934
|
+
this.debugLog("info", `[flag-eval] "${flag.key}": DISABLED (beta-only, ${userEmail} not in beta list)`);
|
|
1935
|
+
return false;
|
|
1936
|
+
}
|
|
1937
|
+
this.debugLog("info", `[flag-eval] "${flag.key}": beta check passed for ${userEmail}`);
|
|
1938
|
+
}
|
|
1873
1939
|
if (flag.targetAttributes && flag.targetAttributes.length > 0) {
|
|
1874
|
-
const attrs = (
|
|
1940
|
+
const attrs = (_b = this.config.customAttributes) != null ? _b : {};
|
|
1875
1941
|
const allMatch = flag.targetAttributes.every((rule) => {
|
|
1876
1942
|
const actual = attrs[rule.attribute];
|
|
1877
1943
|
if (actual === void 0) return false;
|
|
@@ -1890,18 +1956,27 @@ var _SiteManager = class _SiteManager {
|
|
|
1890
1956
|
return false;
|
|
1891
1957
|
}
|
|
1892
1958
|
});
|
|
1893
|
-
if (!allMatch)
|
|
1959
|
+
if (!allMatch) {
|
|
1960
|
+
this.debugLog("info", `[flag-eval] "${flag.key}": DISABLED (custom attribute rules not matched)`);
|
|
1961
|
+
return false;
|
|
1962
|
+
}
|
|
1963
|
+
this.debugLog("info", `[flag-eval] "${flag.key}": custom attribute rules matched`);
|
|
1894
1964
|
}
|
|
1895
1965
|
if (flag.targetEmails && flag.targetEmails.length > 0) {
|
|
1896
1966
|
if (!userEmail || !flag.targetEmails.includes(userEmail)) {
|
|
1967
|
+
this.debugLog("info", `[flag-eval] "${flag.key}": DISABLED (not in targetEmails)`);
|
|
1897
1968
|
return false;
|
|
1898
1969
|
}
|
|
1970
|
+
this.debugLog("info", `[flag-eval] "${flag.key}": ENABLED (user in targetEmails)`);
|
|
1899
1971
|
return true;
|
|
1900
1972
|
}
|
|
1901
1973
|
if (flag.rolloutPercentage !== void 0 && flag.rolloutPercentage < 100) {
|
|
1902
1974
|
const bucket = this.getUserBucket(flag.key);
|
|
1903
|
-
|
|
1975
|
+
const enabled = bucket < flag.rolloutPercentage;
|
|
1976
|
+
this.debugLog("info", `[flag-eval] "${flag.key}": ${enabled ? "ENABLED" : "DISABLED"} (rollout ${flag.rolloutPercentage}%, bucket ${bucket})`);
|
|
1977
|
+
return enabled;
|
|
1904
1978
|
}
|
|
1979
|
+
this.debugLog("info", `[flag-eval] "${flag.key}": ENABLED (no restrictions)`);
|
|
1905
1980
|
return true;
|
|
1906
1981
|
}
|
|
1907
1982
|
/**
|
|
@@ -1918,6 +1993,16 @@ var _SiteManager = class _SiteManager {
|
|
|
1918
1993
|
this.featureFlagBuckets.set(flagKey, bucket);
|
|
1919
1994
|
return bucket;
|
|
1920
1995
|
}
|
|
1996
|
+
/**
|
|
1997
|
+
* Check if the current environment is localhost
|
|
1998
|
+
*/
|
|
1999
|
+
isLocalhost() {
|
|
2000
|
+
if (typeof window === "undefined") {
|
|
2001
|
+
return false;
|
|
2002
|
+
}
|
|
2003
|
+
const hostname = window.location.hostname;
|
|
2004
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
2005
|
+
}
|
|
1921
2006
|
/**
|
|
1922
2007
|
* Simple hash function for consistent bucketing
|
|
1923
2008
|
*/
|
|
@@ -2128,7 +2213,7 @@ var _SiteManager = class _SiteManager {
|
|
|
2128
2213
|
const regex = new RegExp(escapedPattern);
|
|
2129
2214
|
return regex.test(currentPath);
|
|
2130
2215
|
} catch (e) {
|
|
2131
|
-
|
|
2216
|
+
this.debugLog("warn", `[message] Invalid pattern: ${pattern}`);
|
|
2132
2217
|
return false;
|
|
2133
2218
|
}
|
|
2134
2219
|
});
|
|
@@ -2232,7 +2317,7 @@ var _SiteManager = class _SiteManager {
|
|
|
2232
2317
|
}
|
|
2233
2318
|
handleMessageAction(action, message) {
|
|
2234
2319
|
var _a;
|
|
2235
|
-
|
|
2320
|
+
this.debugLog("info", `[message-action] ${action} on message ${message.id}`);
|
|
2236
2321
|
if ((_a = message.cta) == null ? void 0 : _a.url) {
|
|
2237
2322
|
window.location.href = message.cta.url;
|
|
2238
2323
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "featurely-site-manager",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
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",
|
|
@@ -57,6 +57,8 @@
|
|
|
57
57
|
"typescript": "^5.0.0"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"dompurify": "^3.3.3"
|
|
60
|
+
"dompurify": "^3.3.3",
|
|
61
|
+
"featurely-error-tracker": "^1.0.23",
|
|
62
|
+
"featurely-mcp": "^1.0.6"
|
|
61
63
|
}
|
|
62
64
|
}
|