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 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 | 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 |
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, // 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
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('purchase', 4999, 'USD', {
230
- productId: 'pro-plan',
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('signup_completed', {
236
- plan: 'pro',
237
- source: 'landing-page',
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.29";
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.29";
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.29";
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
- console.error(
542
- `[Featurely] Invalid or missing API key. ${message}
543
- \u2192 Check your API key at https://www.featurely.no/dashboard/settings`
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
- console.error(
548
- `[Featurely] Permission denied. ${message}
549
- \u2192 Ensure your API key has the required permissions at https://www.featurely.no/dashboard/settings`
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
- const cspMsg = `[Featurely] Network error \u2014 request to Featurely was blocked.
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
- console.error("Featurely Site Manager: Failed to fetch configuration", error);
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] silencing errors after ${_SiteManager.MAX_CONSECUTIVE_FAILURES} consecutive failures`
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] v1.1.19 | project: ${this.config.projectId}`);
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", "1.1.16")}
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">&#x2b21; 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 ? "&#x2b06;" : "&#x2b07;"}</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">&times;</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
- console.warn("Featurely Site Manager: appVersion not provided for version check");
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
- console.error("Error checking version:", error);
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 = (_a = this.config.customAttributes) != null ? _a : {};
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) return false;
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
- return bucket < flag.rolloutPercentage;
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
- console.warn("Invalid pattern:", pattern, e);
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
- console.log("Message action triggered:", action, message);
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.29";
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
- console.error(
506
- `[Featurely] Invalid or missing API key. ${message}
507
- \u2192 Check your API key at https://www.featurely.no/dashboard/settings`
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
- console.error(
512
- `[Featurely] Permission denied. ${message}
513
- \u2192 Ensure your API key has the required permissions at https://www.featurely.no/dashboard/settings`
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
- const cspMsg = `[Featurely] Network error \u2014 request to Featurely was blocked.
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
- console.error("Featurely Site Manager: Failed to fetch configuration", error);
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] silencing errors after ${_SiteManager.MAX_CONSECUTIVE_FAILURES} consecutive failures`
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] v1.1.19 | project: ${this.config.projectId}`);
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", "1.1.16")}
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">&#x2b21; 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 ? "&#x2b06;" : "&#x2b07;"}</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">&times;</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
- console.warn("Featurely Site Manager: appVersion not provided for version check");
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
- console.error("Error checking version:", error);
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 = (_a = this.config.customAttributes) != null ? _a : {};
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) return false;
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
- return bucket < flag.rolloutPercentage;
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
- console.warn("Invalid pattern:", pattern, e);
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
- console.log("Message action triggered:", action, message);
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.2.0",
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
  }