featurely-site-manager 1.3.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/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- declare const SDK_VERSION = "1.3.0";
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";
@@ -31,6 +31,7 @@ interface FeatureFlag {
31
31
  targetEmails?: string[];
32
32
  excludeEmails?: string[];
33
33
  betaUsersOnly?: boolean;
34
+ availableOnLocalhost?: boolean;
34
35
  variants?: {
35
36
  key: string;
36
37
  name: string;
@@ -247,6 +248,7 @@ declare class SiteManager {
247
248
  private matchHostnamePattern;
248
249
  private evaluateFeatureFlag;
249
250
  private getUserBucket;
251
+ private isLocalhost;
250
252
  private simpleHash;
251
253
  private getAnonymousId;
252
254
  private getOrCreateVisitorId;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- declare const SDK_VERSION = "1.3.0";
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";
@@ -31,6 +31,7 @@ interface FeatureFlag {
31
31
  targetEmails?: string[];
32
32
  excludeEmails?: string[];
33
33
  betaUsersOnly?: boolean;
34
+ availableOnLocalhost?: boolean;
34
35
  variants?: {
35
36
  key: string;
36
37
  name: string;
@@ -247,6 +248,7 @@ declare class SiteManager {
247
248
  private matchHostnamePattern;
248
249
  private evaluateFeatureFlag;
249
250
  private getUserBucket;
251
+ private isLocalhost;
250
252
  private simpleHash;
251
253
  private getAnonymousId;
252
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.3.0";
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")
@@ -1899,21 +1947,30 @@ var _SiteManager = class _SiteManager {
1899
1947
  */
1900
1948
  evaluateFeatureFlag(flag) {
1901
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
  }
1909
1963
  if (flag.betaUsersOnly) {
1910
1964
  if (!userEmail) {
1965
+ this.debugLog("info", `[flag-eval] "${flag.key}": DISABLED (beta-only, no email provided)`);
1911
1966
  return false;
1912
1967
  }
1913
1968
  const betaUsers = ((_a = this.siteConfig) == null ? void 0 : _a.betaUserEmails) || [];
1914
1969
  if (!betaUsers.includes(userEmail)) {
1970
+ this.debugLog("info", `[flag-eval] "${flag.key}": DISABLED (beta-only, ${userEmail} not in beta list)`);
1915
1971
  return false;
1916
1972
  }
1973
+ this.debugLog("info", `[flag-eval] "${flag.key}": beta check passed for ${userEmail}`);
1917
1974
  }
1918
1975
  if (flag.targetAttributes && flag.targetAttributes.length > 0) {
1919
1976
  const attrs = (_b = this.config.customAttributes) != null ? _b : {};
@@ -1935,18 +1992,27 @@ var _SiteManager = class _SiteManager {
1935
1992
  return false;
1936
1993
  }
1937
1994
  });
1938
- 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`);
1939
2000
  }
1940
2001
  if (flag.targetEmails && flag.targetEmails.length > 0) {
1941
2002
  if (!userEmail || !flag.targetEmails.includes(userEmail)) {
2003
+ this.debugLog("info", `[flag-eval] "${flag.key}": DISABLED (not in targetEmails)`);
1942
2004
  return false;
1943
2005
  }
2006
+ this.debugLog("info", `[flag-eval] "${flag.key}": ENABLED (user in targetEmails)`);
1944
2007
  return true;
1945
2008
  }
1946
2009
  if (flag.rolloutPercentage !== void 0 && flag.rolloutPercentage < 100) {
1947
2010
  const bucket = this.getUserBucket(flag.key);
1948
- 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;
1949
2014
  }
2015
+ this.debugLog("info", `[flag-eval] "${flag.key}": ENABLED (no restrictions)`);
1950
2016
  return true;
1951
2017
  }
1952
2018
  /**
@@ -1963,6 +2029,16 @@ var _SiteManager = class _SiteManager {
1963
2029
  this.featureFlagBuckets.set(flagKey, bucket);
1964
2030
  return bucket;
1965
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
+ }
1966
2042
  /**
1967
2043
  * Simple hash function for consistent bucketing
1968
2044
  */
@@ -2173,7 +2249,7 @@ var _SiteManager = class _SiteManager {
2173
2249
  const regex = new RegExp(escapedPattern);
2174
2250
  return regex.test(currentPath);
2175
2251
  } catch (e) {
2176
- console.warn("Invalid pattern:", pattern, e);
2252
+ this.debugLog("warn", `[message] Invalid pattern: ${pattern}`);
2177
2253
  return false;
2178
2254
  }
2179
2255
  });
@@ -2277,7 +2353,7 @@ var _SiteManager = class _SiteManager {
2277
2353
  }
2278
2354
  handleMessageAction(action, message) {
2279
2355
  var _a;
2280
- console.log("Message action triggered:", action, message);
2356
+ this.debugLog("info", `[message-action] ${action} on message ${message.id}`);
2281
2357
  if ((_a = message.cta) == null ? void 0 : _a.url) {
2282
2358
  window.location.href = message.cta.url;
2283
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.3.0";
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")
@@ -1863,21 +1911,30 @@ var _SiteManager = class _SiteManager {
1863
1911
  */
1864
1912
  evaluateFeatureFlag(flag) {
1865
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
  }
1873
1927
  if (flag.betaUsersOnly) {
1874
1928
  if (!userEmail) {
1929
+ this.debugLog("info", `[flag-eval] "${flag.key}": DISABLED (beta-only, no email provided)`);
1875
1930
  return false;
1876
1931
  }
1877
1932
  const betaUsers = ((_a = this.siteConfig) == null ? void 0 : _a.betaUserEmails) || [];
1878
1933
  if (!betaUsers.includes(userEmail)) {
1934
+ this.debugLog("info", `[flag-eval] "${flag.key}": DISABLED (beta-only, ${userEmail} not in beta list)`);
1879
1935
  return false;
1880
1936
  }
1937
+ this.debugLog("info", `[flag-eval] "${flag.key}": beta check passed for ${userEmail}`);
1881
1938
  }
1882
1939
  if (flag.targetAttributes && flag.targetAttributes.length > 0) {
1883
1940
  const attrs = (_b = this.config.customAttributes) != null ? _b : {};
@@ -1899,18 +1956,27 @@ var _SiteManager = class _SiteManager {
1899
1956
  return false;
1900
1957
  }
1901
1958
  });
1902
- 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`);
1903
1964
  }
1904
1965
  if (flag.targetEmails && flag.targetEmails.length > 0) {
1905
1966
  if (!userEmail || !flag.targetEmails.includes(userEmail)) {
1967
+ this.debugLog("info", `[flag-eval] "${flag.key}": DISABLED (not in targetEmails)`);
1906
1968
  return false;
1907
1969
  }
1970
+ this.debugLog("info", `[flag-eval] "${flag.key}": ENABLED (user in targetEmails)`);
1908
1971
  return true;
1909
1972
  }
1910
1973
  if (flag.rolloutPercentage !== void 0 && flag.rolloutPercentage < 100) {
1911
1974
  const bucket = this.getUserBucket(flag.key);
1912
- 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;
1913
1978
  }
1979
+ this.debugLog("info", `[flag-eval] "${flag.key}": ENABLED (no restrictions)`);
1914
1980
  return true;
1915
1981
  }
1916
1982
  /**
@@ -1927,6 +1993,16 @@ var _SiteManager = class _SiteManager {
1927
1993
  this.featureFlagBuckets.set(flagKey, bucket);
1928
1994
  return bucket;
1929
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
+ }
1930
2006
  /**
1931
2007
  * Simple hash function for consistent bucketing
1932
2008
  */
@@ -2137,7 +2213,7 @@ var _SiteManager = class _SiteManager {
2137
2213
  const regex = new RegExp(escapedPattern);
2138
2214
  return regex.test(currentPath);
2139
2215
  } catch (e) {
2140
- console.warn("Invalid pattern:", pattern, e);
2216
+ this.debugLog("warn", `[message] Invalid pattern: ${pattern}`);
2141
2217
  return false;
2142
2218
  }
2143
2219
  });
@@ -2241,7 +2317,7 @@ var _SiteManager = class _SiteManager {
2241
2317
  }
2242
2318
  handleMessageAction(action, message) {
2243
2319
  var _a;
2244
- console.log("Message action triggered:", action, message);
2320
+ this.debugLog("info", `[message-action] ${action} on message ${message.id}`);
2245
2321
  if ((_a = message.cta) == null ? void 0 : _a.url) {
2246
2322
  window.location.href = message.cta.url;
2247
2323
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "featurely-site-manager",
3
- "version": "1.3.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",
@@ -59,7 +59,6 @@
59
59
  "dependencies": {
60
60
  "dompurify": "^3.3.3",
61
61
  "featurely-error-tracker": "^1.0.23",
62
- "featurely-mcp": "^1.0.6",
63
- "featurely-site-manager": "^1.2.0"
62
+ "featurely-mcp": "^1.0.6"
64
63
  }
65
64
  }