featurely-site-manager 1.1.18 → 1.1.20

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
@@ -69,11 +69,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
69
69
  - **CRITICAL FIX**: Replaced Math.random() with crypto.getRandomValues() for CSPRNG session IDs and anonymous IDs
70
70
  - **CRITICAL FIX**: Added RegExp pattern escaping to prevent ReDoS (Regular Expression Denial of Service) attacks
71
71
  - Added error handling for invalid targetPages patterns
72
+
72
73
  ## [1.0.5] - 2026-03-15
73
74
 
74
75
  ### Security
75
76
 
76
- - **CRITICAL FIXAdded DOMPurify sanitization for customHtml maintenance pages to prevent XSS attacks
77
+ - \*\*CRITICAL FIXAdded DOMPurify sanitization for customHtml maintenance pages to prevent XSS attacks
77
78
  - Blocks dangerous tags (script, iframe, embed, object, etc.)
78
79
  - Removes event handlers (onerror, onload, onclick, etc.)
79
80
  - Prevents javascript: URIs and other malicious protocols
package/README.md CHANGED
@@ -179,8 +179,12 @@ const manager = new SiteManager({
179
179
  onUpdateRequired: (versionInfo) => {
180
180
  console.log("Update required:", versionInfo.latestVersion);
181
181
  // Force user to update (breaking changes)
182
- if (confirm(`Update required: ${versionInfo.latestVersion.title}\n\n${versionInfo.latestVersion.releaseNotes}`)) {
183
- window.location.href = versionInfo.latestVersion.downloadUrl || '/update';
182
+ if (
183
+ confirm(
184
+ `Update required: ${versionInfo.latestVersion.title}\n\n${versionInfo.latestVersion.releaseNotes}`
185
+ )
186
+ ) {
187
+ window.location.href = versionInfo.latestVersion.downloadUrl || "/update";
184
188
  }
185
189
  },
186
190
  });
@@ -255,12 +259,12 @@ manager.trackEvent("purchase_completed", {
255
259
 
256
260
  When `enableAnalytics` is `true` (default), the SDK automatically tracks:
257
261
 
258
- | Event | When | Properties |
259
- | --- | --- | --- |
260
- | `session_start` | On first page load | `path`, `title`, `referrer` |
261
- | `page_view` | On every page navigation | `path`, `title`, `referrer`, `isLoggedIn`, `userId?`, `userEmail?` |
262
- | `page_exit` | When navigating away from a page | `path`, `durationSeconds` |
263
- | `user_login` | First time `setUser()` is called with a userId | `userId`, `userEmail?`, `userName?` |
262
+ | Event | When | Properties |
263
+ | --------------- | ---------------------------------------------- | ------------------------------------------------------------------ |
264
+ | `session_start` | On first page load | `path`, `title`, `referrer` |
265
+ | `page_view` | On every page navigation | `path`, `title`, `referrer`, `isLoggedIn`, `userId?`, `userEmail?` |
266
+ | `page_exit` | When navigating away from a page | `path`, `durationSeconds` |
267
+ | `user_login` | First time `setUser()` is called with a userId | `userId`, `userEmail?`, `userName?` |
264
268
 
265
269
  Page tracking is SPA-compatible — it patches `history.pushState` and `history.replaceState` and listens to `popstate` so navigations in React, Next.js, Vue, and similar frameworks are captured automatically.
266
270
 
@@ -282,13 +286,13 @@ manager.init();
282
286
 
283
287
  The overlay panel has five tabs:
284
288
 
285
- | Tab | Contents |
286
- | --- | --- |
287
- | **SDK** | Environment, maintenance status, active messages, feature flag count |
288
- | **Logs** | Real-time log stream (info / warn / error); error badge on tab; Clear button |
289
- | **Network** | Config fetch log with status codes and timestamps |
290
- | **Events** | Analytics events queued/sent |
291
- | **Test** | Buttons to test analytics, errors, bugs, feature flags, flush, and refresh |
289
+ | Tab | Contents |
290
+ | ----------- | ---------------------------------------------------------------------------- |
291
+ | **SDK** | Environment, maintenance status, active messages, feature flag count |
292
+ | **Logs** | Real-time log stream (info / warn / error); error badge on tab; Clear button |
293
+ | **Network** | Config fetch log with status codes and timestamps |
294
+ | **Events** | Analytics events queued/sent |
295
+ | **Test** | Buttons to test analytics, errors, bugs, feature flags, flush, and refresh |
292
296
 
293
297
  #### Test Tab Actions
294
298
 
@@ -332,29 +336,29 @@ manager.init();
332
336
 
333
337
  ### SiteManagerConfig
334
338
 
335
- | Option | Type | Required | Default | Description |
336
- | ------------------------ | --------------- | -------- | ------------------------- | ----------------------------------------- |
337
- | `apiKey` | `string` | ✅ | - | Your Featurely API key |
338
- | `projectId` | `string` | ✅ | - | Your Featurely project ID |
339
+ | Option | Type | Required | Default | Description |
340
+ | ------------------------ | --------------- | -------- | ------------------------ | ----------------------------------------- |
341
+ | `apiKey` | `string` | ✅ | - | Your Featurely API key |
342
+ | `projectId` | `string` | ✅ | - | Your Featurely project ID |
339
343
  | `apiUrl` | `string` | ❌ | `'https://featurely.no'` | Custom API endpoint |
340
- | `pollInterval` | `number` | ❌ | `60000` | Config polling interval in ms |
341
- | `userEmail` | `string` | ❌ | - | User email for whitelist & targeting |
342
- | `userId` | `string` | ❌ | - | User ID for analytics & feature flags |
343
- | `bypassCheck` | `() => boolean` | ❌ | - | Custom maintenance bypass function |
344
- | `onMaintenanceEnabled` | `function` | ❌ | - | Maintenance enabled callback |
345
- | `onMaintenanceDisabled` | `function` | ❌ | - | Maintenance disabled callback |
346
- | `onMessageReceived` | `function` | ❌ | - | Message received callback |
347
- | `onMessageDismissed` | `function` | ❌ | - | Message dismissed callback |
348
- | `onFeatureFlagsUpdated` | `function` | ❌ | - | Feature flags updated callback |
349
- | `enableAnalytics` | `boolean` | ❌ | `true` | Enable analytics tracking |
350
- | `analyticsFlushInterval` | `number` | ❌ | `60000` | Analytics flush interval in ms |
351
- | `appVersion` | `string` | ❌ | - | Current app version for version checking |
352
- | `enableVersionCheck` | `boolean` | ❌ | `false` | Enable automatic version checking |
353
- | `versionCheckInterval` | `number` | ❌ | `3600000` | Version check interval in ms (1 hour) |
354
- | `onUpdateAvailable` | `function` | ❌ | - | Callback when update is available |
355
- | `onUpdateRequired` | `function` | ❌ | - | Callback when update is required (forced) |
356
- | `onError` | `function` | ❌ | - | Error callback |
357
- | `debugMode` | `boolean` | ❌ | `false` | Force-enable debug overlay locally |
344
+ | `pollInterval` | `number` | ❌ | `60000` | Config polling interval in ms |
345
+ | `userEmail` | `string` | ❌ | - | User email for whitelist & targeting |
346
+ | `userId` | `string` | ❌ | - | User ID for analytics & feature flags |
347
+ | `bypassCheck` | `() => boolean` | ❌ | - | Custom maintenance bypass function |
348
+ | `onMaintenanceEnabled` | `function` | ❌ | - | Maintenance enabled callback |
349
+ | `onMaintenanceDisabled` | `function` | ❌ | - | Maintenance disabled callback |
350
+ | `onMessageReceived` | `function` | ❌ | - | Message received callback |
351
+ | `onMessageDismissed` | `function` | ❌ | - | Message dismissed callback |
352
+ | `onFeatureFlagsUpdated` | `function` | ❌ | - | Feature flags updated callback |
353
+ | `enableAnalytics` | `boolean` | ❌ | `true` | Enable analytics tracking |
354
+ | `analyticsFlushInterval` | `number` | ❌ | `60000` | Analytics flush interval in ms |
355
+ | `appVersion` | `string` | ❌ | - | Current app version for version checking |
356
+ | `enableVersionCheck` | `boolean` | ❌ | `false` | Enable automatic version checking |
357
+ | `versionCheckInterval` | `number` | ❌ | `3600000` | Version check interval in ms (1 hour) |
358
+ | `onUpdateAvailable` | `function` | ❌ | - | Callback when update is available |
359
+ | `onUpdateRequired` | `function` | ❌ | - | Callback when update is required (forced) |
360
+ | `onError` | `function` | ❌ | - | Error callback |
361
+ | `debugMode` | `boolean` | ❌ | `false` | Force-enable debug overlay locally |
358
362
 
359
363
  ## 🎯 API Methods
360
364
 
package/dist/index.js CHANGED
@@ -115,8 +115,14 @@ var _SiteManager = class _SiteManager {
115
115
  if (this.config.debugMode) {
116
116
  this.setupDebugOverlay();
117
117
  }
118
- this.debugLog("info", `[init] v1.1.16 | project: ${this.config.projectId} | hostname: ${typeof window !== "undefined" ? window.location.hostname : "\u2014"}`);
119
- this.debugLog("info", `[init] analytics: ${this.config.enableAnalytics ? "on" : "off"} | poll: ${this.config.pollInterval}ms | debug: ${this.config.debugMode ? "on" : "off"}`);
118
+ this.debugLog(
119
+ "info",
120
+ `[init] v1.1.19 | project: ${this.config.projectId} | hostname: ${typeof window !== "undefined" ? window.location.hostname : "\u2014"}`
121
+ );
122
+ this.debugLog(
123
+ "info",
124
+ `[init] analytics: ${this.config.enableAnalytics ? "on" : "off"} | poll: ${this.config.pollInterval}ms | debug: ${this.config.debugMode ? "on" : "off"}`
125
+ );
120
126
  if (this.config.enableAnalytics) {
121
127
  this.debugLog("info", "[init] starting analytics + page tracking");
122
128
  this.startAnalyticsFlushing();
@@ -127,7 +133,10 @@ var _SiteManager = class _SiteManager {
127
133
  this.startPolling();
128
134
  this.debugLog("info", `[init] polling every ${this.config.pollInterval / 1e3}s`);
129
135
  if (this.config.enableVersionCheck && this.config.appVersion) {
130
- this.debugLog("info", `[init] checking version (current: ${this.config.appVersion})`);
136
+ this.debugLog(
137
+ "info",
138
+ `[init] checking version (current: ${this.config.appVersion})`
139
+ );
131
140
  await this.checkVersion();
132
141
  this.startVersionChecking();
133
142
  }
@@ -302,7 +311,10 @@ var _SiteManager = class _SiteManager {
302
311
  properties,
303
312
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
304
313
  });
305
- this.recentAnalyticsEvents.unshift({ name: eventName, ts: (/* @__PURE__ */ new Date()).toLocaleTimeString() });
314
+ this.recentAnalyticsEvents.unshift({
315
+ name: eventName,
316
+ ts: (/* @__PURE__ */ new Date()).toLocaleTimeString()
317
+ });
306
318
  if (this.recentAnalyticsEvents.length > 50) this.recentAnalyticsEvents.length = 50;
307
319
  this.debugLog("info", `trackEvent: ${eventName}`);
308
320
  if (this.analyticsQueue.length >= 10) {
@@ -324,7 +336,11 @@ var _SiteManager = class _SiteManager {
324
336
  }
325
337
  }
326
338
  );
327
- this.debugNetwork(`/api/public/v1/site-config`, response.status, Date.now() - fetchStartMs);
339
+ this.debugNetwork(
340
+ `/api/public/v1/site-config`,
341
+ response.status,
342
+ Date.now() - fetchStartMs
343
+ );
328
344
  if (!response.ok) {
329
345
  const errorData = await response.json().catch(() => ({}));
330
346
  const message = errorData.error || response.statusText;
@@ -357,20 +373,55 @@ var _SiteManager = class _SiteManager {
357
373
  delta.push(`flags: ${prevFlagCount}\u2192${newConfig.featureFlags.length}`);
358
374
  if (newConfig.messages.length !== prevMsgCount)
359
375
  delta.push(`messages: ${prevMsgCount}\u2192${newConfig.messages.length}`);
360
- this.debugLog("info", `[config] updated${delta.length ? ": " + delta.join(", ") : " (structure changed)"}`);
376
+ this.debugLog(
377
+ "info",
378
+ `[config] updated${delta.length ? ": " + delta.join(", ") : " (structure changed)"}`
379
+ );
361
380
  const wasMaintenanceEnabled = (_g = this.siteConfig) == null ? void 0 : _g.maintenance.enabled;
362
381
  const oldFeatureFlags = ((_h = this.siteConfig) == null ? void 0 : _h.featureFlags) || [];
363
382
  this.siteConfig = newConfig;
364
- if (newConfig.debugMode === true && !this.debugOverlayEl) {
383
+ const isGlobalDebug = newConfig.debugMode === true;
384
+ const hostname = typeof window !== "undefined" ? window.location.hostname : "";
385
+ const envs = newConfig.environments;
386
+ const matchedEnv = hostname ? envs == null ? void 0 : envs.find((e) => {
387
+ if (typeof e.url !== "string" || e.url === "") return false;
388
+ let storedHost;
389
+ try {
390
+ storedHost = e.url.includes("://") ? new URL(e.url).hostname : e.url;
391
+ } catch {
392
+ storedHost = e.url;
393
+ }
394
+ return storedHost === hostname;
395
+ }) : void 0;
396
+ const isEnvDebug = (matchedEnv == null ? void 0 : matchedEnv.debugEnabled) === true;
397
+ const shouldDebug = isGlobalDebug || isEnvDebug;
398
+ if (shouldDebug && !this.debugOverlayEl) {
399
+ if (isEnvDebug && !isGlobalDebug) {
400
+ this.debugLog(
401
+ "info",
402
+ `[env] matched environment "${matchedEnv.name}" (${hostname}) \u2014 enabling debug overlay`
403
+ );
404
+ }
365
405
  this.setupDebugOverlay();
406
+ } else if (!shouldDebug && this.debugOverlayEl) {
407
+ this.debugLog("info", "[debug] debug disabled by config \u2014 removing overlay");
408
+ this.stopDebugOverlay();
366
409
  }
367
- if (!this.debugOverlayEl) {
368
- const hostname = typeof window !== "undefined" ? window.location.hostname : "";
369
- const envs = newConfig.environments;
370
- const matchedEnv = hostname ? envs == null ? void 0 : envs.find((e) => typeof e.url === "string" && e.url !== "" && e.url === hostname) : void 0;
371
- if ((matchedEnv == null ? void 0 : matchedEnv.debugEnabled) === true) {
372
- this.debugLog("info", `[env] matched environment "${matchedEnv.name}" (${hostname})`);
373
- this.setupDebugOverlay();
410
+ if (matchedEnv && matchedEnv.analyticsEnabled === false) {
411
+ if (this.config.enableAnalytics) {
412
+ this.debugLog(
413
+ "info",
414
+ `[env] analytics disabled for environment "${matchedEnv.name}" (${hostname}) \u2014 stopping analytics`
415
+ );
416
+ this.stopAnalyticsFlushing();
417
+ }
418
+ } else if (matchedEnv && matchedEnv.analyticsEnabled !== false && this.config.enableAnalytics) {
419
+ if (!this.analyticsFlushIntervalId) {
420
+ this.debugLog(
421
+ "info",
422
+ `[env] analytics re-enabled for environment "${matchedEnv.name}" (${hostname})`
423
+ );
424
+ this.startAnalyticsFlushing();
374
425
  }
375
426
  }
376
427
  if (newConfig.maintenance.enabled && !wasMaintenanceEnabled) {
@@ -392,7 +443,10 @@ var _SiteManager = class _SiteManager {
392
443
  } catch (error) {
393
444
  this.consecutiveFetchFailures++;
394
445
  const errMsg = error instanceof Error ? error.message : String(error);
395
- this.debugLog("error", `[config] fetch failed (attempt ${this.consecutiveFetchFailures}): ${errMsg}`);
446
+ this.debugLog(
447
+ "error",
448
+ `[config] fetch failed (attempt ${this.consecutiveFetchFailures}): ${errMsg}`
449
+ );
396
450
  this.errorCount++;
397
451
  if (this.consecutiveFetchFailures <= _SiteManager.MAX_CONSECUTIVE_FAILURES) {
398
452
  const isNetworkError = error instanceof TypeError && (error.message.includes("NetworkError") || error.message.includes("Failed to fetch") || error.message.includes("fetch"));
@@ -411,7 +465,10 @@ var _SiteManager = class _SiteManager {
411
465
  } else if (this.consecutiveFetchFailures === _SiteManager.MAX_CONSECUTIVE_FAILURES + 1) {
412
466
  const silenceMsg = `Featurely Site Manager: Silencing repeated fetch errors after ${_SiteManager.MAX_CONSECUTIVE_FAILURES} failures. Check your apiUrl and Content-Security-Policy configuration.`;
413
467
  console.error(silenceMsg);
414
- this.debugLog("error", `[config] silencing errors after ${_SiteManager.MAX_CONSECUTIVE_FAILURES} consecutive failures`);
468
+ this.debugLog(
469
+ "error",
470
+ `[config] silencing errors after ${_SiteManager.MAX_CONSECUTIVE_FAILURES} consecutive failures`
471
+ );
415
472
  }
416
473
  }
417
474
  }
@@ -450,7 +507,10 @@ var _SiteManager = class _SiteManager {
450
507
  const eventsToSend = [...this.analyticsQueue];
451
508
  this.analyticsQueue = [];
452
509
  this.analyticsEventsSent += eventsToSend.length;
453
- this.debugLog("info", `[analytics] flushing ${eventsToSend.length} event(s) (total sent: ${this.analyticsEventsSent})`);
510
+ this.debugLog(
511
+ "info",
512
+ `[analytics] flushing ${eventsToSend.length} event(s) (total sent: ${this.analyticsEventsSent})`
513
+ );
454
514
  for (const event of eventsToSend) {
455
515
  const payload = JSON.stringify({
456
516
  eventName: event.eventName,
@@ -474,7 +534,11 @@ var _SiteManager = class _SiteManager {
474
534
  body: payload,
475
535
  keepalive: true
476
536
  });
477
- this.debugNetwork(`/api/projects/${this.config.projectId}/analytics/events`, res.status, Date.now() - t0);
537
+ this.debugNetwork(
538
+ `/api/projects/${this.config.projectId}/analytics/events`,
539
+ res.status,
540
+ Date.now() - t0
541
+ );
478
542
  if (!res.ok) {
479
543
  const body = await res.text().catch(() => "(unreadable)");
480
544
  const errDetail = `[analytics] event "${event.eventName}" rejected: ${res.status} ${res.statusText} \u2014 ${body}`;
@@ -486,11 +550,20 @@ var _SiteManager = class _SiteManager {
486
550
  const sentViaBeacon = typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && navigator.sendBeacon(url, payload);
487
551
  const errMsg = fetchError instanceof Error ? fetchError.message : String(fetchError);
488
552
  if (!sentViaBeacon) {
489
- console.error(`[Featurely] Failed to send analytics event "${event.eventName}":`, fetchError);
490
- this.debugLog("error", `[analytics] send failed for "${event.eventName}": ${errMsg}`);
553
+ console.error(
554
+ `[Featurely] Failed to send analytics event "${event.eventName}":`,
555
+ fetchError
556
+ );
557
+ this.debugLog(
558
+ "error",
559
+ `[analytics] send failed for "${event.eventName}": ${errMsg}`
560
+ );
491
561
  this.errorCount++;
492
562
  } else {
493
- this.debugLog("warn", `[analytics] fetch failed for "${event.eventName}" (${errMsg}), delivered via sendBeacon`);
563
+ this.debugLog(
564
+ "warn",
565
+ `[analytics] fetch failed for "${event.eventName}" (${errMsg}), delivered via sendBeacon`
566
+ );
494
567
  }
495
568
  }
496
569
  }
@@ -537,14 +610,18 @@ var _SiteManager = class _SiteManager {
537
610
  this.originalConsoleError = console.error.bind(console);
538
611
  console.error = (...args) => {
539
612
  this.originalConsoleError(...args);
540
- const msg = args.map((a) => typeof a === "string" ? a : a instanceof Error ? `${a.message}` : JSON.stringify(a)).join(" ");
613
+ const msg = args.map(
614
+ (a) => typeof a === "string" ? a : a instanceof Error ? `${a.message}` : JSON.stringify(a)
615
+ ).join(" ");
541
616
  this.errorCount++;
542
617
  this.debugLog("error", `[console.error] ${msg.slice(0, 300)}`);
543
618
  };
544
619
  this.originalConsoleWarn = console.warn.bind(console);
545
620
  console.warn = (...args) => {
546
621
  this.originalConsoleWarn(...args);
547
- const msg = args.map((a) => typeof a === "string" ? a : a instanceof Error ? `${a.message}` : JSON.stringify(a)).join(" ");
622
+ const msg = args.map(
623
+ (a) => typeof a === "string" ? a : a instanceof Error ? `${a.message}` : JSON.stringify(a)
624
+ ).join(" ");
548
625
  this.debugLog("warn", `[console.warn] ${msg.slice(0, 300)}`);
549
626
  };
550
627
  window.addEventListener("error", (e) => {
@@ -597,7 +674,7 @@ var _SiteManager = class _SiteManager {
597
674
  this.debugOverlayEl = el;
598
675
  this.setupGlobalErrorCapture();
599
676
  this.debugLog("info", `[site-manager] debug overlay initialized`);
600
- this.debugLog("info", `[site-manager] v1.1.14 | project: ${this.config.projectId}`);
677
+ this.debugLog("info", `[site-manager] v1.1.19 | project: ${this.config.projectId}`);
601
678
  this.renderDebugOverlay();
602
679
  this.debugRefreshId = setInterval(() => this.renderDebugOverlay(), 1500);
603
680
  }
@@ -779,7 +856,10 @@ var _SiteManager = class _SiteManager {
779
856
  };
780
857
  switch (action) {
781
858
  case "test-event":
782
- this.trackEvent("debug_test_event", { source: "debug_panel", ts: (/* @__PURE__ */ new Date()).toISOString() });
859
+ this.trackEvent("debug_test_event", {
860
+ source: "debug_panel",
861
+ ts: (/* @__PURE__ */ new Date()).toISOString()
862
+ });
783
863
  feedback("debug_test_event queued", true);
784
864
  break;
785
865
  case "flush": {
@@ -821,7 +901,10 @@ var _SiteManager = class _SiteManager {
821
901
  case "copy-logs": {
822
902
  const text = this.debugLogs.slice().reverse().map((l) => `[${l.ts}] [${l.level.toUpperCase()}] ${l.msg}`).join("\n");
823
903
  void navigator.clipboard.writeText(text).then(() => {
824
- feedback(`Copied ${this.debugLogs.length} log line${this.debugLogs.length !== 1 ? "s" : ""}`, true);
904
+ feedback(
905
+ `Copied ${this.debugLogs.length} log line${this.debugLogs.length !== 1 ? "s" : ""}`,
906
+ true
907
+ );
825
908
  }).catch(() => {
826
909
  feedback("Copy failed \u2014 clipboard not available", false);
827
910
  });
@@ -939,9 +1022,7 @@ var _SiteManager = class _SiteManager {
939
1022
  async checkVersion(currentVersion) {
940
1023
  const versionToCheck = currentVersion || this.config.appVersion;
941
1024
  if (!versionToCheck) {
942
- console.warn(
943
- "Featurely Site Manager: appVersion not provided for version check"
944
- );
1025
+ console.warn("Featurely Site Manager: appVersion not provided for version check");
945
1026
  return null;
946
1027
  }
947
1028
  try {
@@ -1085,7 +1166,10 @@ var _SiteManager = class _SiteManager {
1085
1166
  this.debugLog("info", "[maintenance] ENABLED \u2014 user bypassed (whitelist match)");
1086
1167
  return;
1087
1168
  }
1088
- this.debugLog("warn", `[maintenance] ENABLED (type: ${this.siteConfig.maintenance.type})`);
1169
+ this.debugLog(
1170
+ "warn",
1171
+ `[maintenance] ENABLED (type: ${this.siteConfig.maintenance.type})`
1172
+ );
1089
1173
  this.showMaintenancePage();
1090
1174
  this.trackEvent("maintenance_enabled", {
1091
1175
  maintenanceType: this.siteConfig.maintenance.type
@@ -1157,7 +1241,17 @@ var _SiteManager = class _SiteManager {
1157
1241
  "button"
1158
1242
  ],
1159
1243
  ALLOWED_ATTR: ["class", "id", "href", "src", "alt", "title", "style"],
1160
- FORBID_TAGS: ["script", "iframe", "embed", "object", "applet", "meta", "link", "form", "input"],
1244
+ FORBID_TAGS: [
1245
+ "script",
1246
+ "iframe",
1247
+ "embed",
1248
+ "object",
1249
+ "applet",
1250
+ "meta",
1251
+ "link",
1252
+ "form",
1253
+ "input"
1254
+ ],
1161
1255
  FORBID_ATTR: ["onerror", "onload", "onclick", "onmouseover"],
1162
1256
  ALLOW_DATA_ATTR: false
1163
1257
  });
@@ -1225,10 +1319,16 @@ var _SiteManager = class _SiteManager {
1225
1319
  this.messageContainers.delete(id);
1226
1320
  }
1227
1321
  });
1228
- this.debugLog("info", `[messages] ${activeMessages.length} active (${(_b = (_a = this.siteConfig) == null ? void 0 : _a.messages.length) != null ? _b : 0} total)`);
1322
+ this.debugLog(
1323
+ "info",
1324
+ `[messages] ${activeMessages.length} active (${(_b = (_a = this.siteConfig) == null ? void 0 : _a.messages.length) != null ? _b : 0} total)`
1325
+ );
1229
1326
  activeMessages.forEach((message) => {
1230
1327
  if (!this.messageContainers.has(message.id)) {
1231
- this.debugLog("info", `[messages] showing "${message.title}" (${message.type}, ${message.style})`);
1328
+ this.debugLog(
1329
+ "info",
1330
+ `[messages] showing "${message.title}" (${message.type}, ${message.style})`
1331
+ );
1232
1332
  this.showMessage(message);
1233
1333
  if (this.config.onMessageReceived) {
1234
1334
  this.config.onMessageReceived(message);
@@ -1494,7 +1594,8 @@ var _SiteManager = class _SiteManager {
1494
1594
 
1495
1595
  .featurely-message-body {
1496
1596
  font-size: 14px;
1497
- opacity: 0.9;
1597
+ /* opacity < 1 reduces effective contrast; inherit full color from parent */
1598
+ opacity: 1;
1498
1599
  }
1499
1600
 
1500
1601
  .featurely-message-cta {
@@ -1545,16 +1646,17 @@ var _SiteManager = class _SiteManager {
1545
1646
 
1546
1647
  .featurely-message-warning {
1547
1648
  background: #fff3e0;
1548
- color: #f57c00;
1649
+ /* #803600 on #fff3e0 \u2248 7.8:1 contrast \u2014 WCAG AA compliant */
1650
+ color: #803600;
1549
1651
  }
1550
1652
 
1551
1653
  .featurely-message-warning .featurely-message-cta {
1552
- background: #f57c00;
1654
+ background: #803600;
1553
1655
  color: white;
1554
1656
  }
1555
1657
 
1556
1658
  .featurely-message-warning .featurely-message-cta:hover {
1557
- background: #ef6c00;
1659
+ background: #6b2d00;
1558
1660
  }
1559
1661
 
1560
1662
  .featurely-message-error {
package/dist/index.mjs CHANGED
@@ -80,8 +80,14 @@ var _SiteManager = class _SiteManager {
80
80
  if (this.config.debugMode) {
81
81
  this.setupDebugOverlay();
82
82
  }
83
- this.debugLog("info", `[init] v1.1.16 | project: ${this.config.projectId} | hostname: ${typeof window !== "undefined" ? window.location.hostname : "\u2014"}`);
84
- this.debugLog("info", `[init] analytics: ${this.config.enableAnalytics ? "on" : "off"} | poll: ${this.config.pollInterval}ms | debug: ${this.config.debugMode ? "on" : "off"}`);
83
+ this.debugLog(
84
+ "info",
85
+ `[init] v1.1.19 | project: ${this.config.projectId} | hostname: ${typeof window !== "undefined" ? window.location.hostname : "\u2014"}`
86
+ );
87
+ this.debugLog(
88
+ "info",
89
+ `[init] analytics: ${this.config.enableAnalytics ? "on" : "off"} | poll: ${this.config.pollInterval}ms | debug: ${this.config.debugMode ? "on" : "off"}`
90
+ );
85
91
  if (this.config.enableAnalytics) {
86
92
  this.debugLog("info", "[init] starting analytics + page tracking");
87
93
  this.startAnalyticsFlushing();
@@ -92,7 +98,10 @@ var _SiteManager = class _SiteManager {
92
98
  this.startPolling();
93
99
  this.debugLog("info", `[init] polling every ${this.config.pollInterval / 1e3}s`);
94
100
  if (this.config.enableVersionCheck && this.config.appVersion) {
95
- this.debugLog("info", `[init] checking version (current: ${this.config.appVersion})`);
101
+ this.debugLog(
102
+ "info",
103
+ `[init] checking version (current: ${this.config.appVersion})`
104
+ );
96
105
  await this.checkVersion();
97
106
  this.startVersionChecking();
98
107
  }
@@ -267,7 +276,10 @@ var _SiteManager = class _SiteManager {
267
276
  properties,
268
277
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
269
278
  });
270
- this.recentAnalyticsEvents.unshift({ name: eventName, ts: (/* @__PURE__ */ new Date()).toLocaleTimeString() });
279
+ this.recentAnalyticsEvents.unshift({
280
+ name: eventName,
281
+ ts: (/* @__PURE__ */ new Date()).toLocaleTimeString()
282
+ });
271
283
  if (this.recentAnalyticsEvents.length > 50) this.recentAnalyticsEvents.length = 50;
272
284
  this.debugLog("info", `trackEvent: ${eventName}`);
273
285
  if (this.analyticsQueue.length >= 10) {
@@ -289,7 +301,11 @@ var _SiteManager = class _SiteManager {
289
301
  }
290
302
  }
291
303
  );
292
- this.debugNetwork(`/api/public/v1/site-config`, response.status, Date.now() - fetchStartMs);
304
+ this.debugNetwork(
305
+ `/api/public/v1/site-config`,
306
+ response.status,
307
+ Date.now() - fetchStartMs
308
+ );
293
309
  if (!response.ok) {
294
310
  const errorData = await response.json().catch(() => ({}));
295
311
  const message = errorData.error || response.statusText;
@@ -322,20 +338,55 @@ var _SiteManager = class _SiteManager {
322
338
  delta.push(`flags: ${prevFlagCount}\u2192${newConfig.featureFlags.length}`);
323
339
  if (newConfig.messages.length !== prevMsgCount)
324
340
  delta.push(`messages: ${prevMsgCount}\u2192${newConfig.messages.length}`);
325
- this.debugLog("info", `[config] updated${delta.length ? ": " + delta.join(", ") : " (structure changed)"}`);
341
+ this.debugLog(
342
+ "info",
343
+ `[config] updated${delta.length ? ": " + delta.join(", ") : " (structure changed)"}`
344
+ );
326
345
  const wasMaintenanceEnabled = (_g = this.siteConfig) == null ? void 0 : _g.maintenance.enabled;
327
346
  const oldFeatureFlags = ((_h = this.siteConfig) == null ? void 0 : _h.featureFlags) || [];
328
347
  this.siteConfig = newConfig;
329
- if (newConfig.debugMode === true && !this.debugOverlayEl) {
348
+ const isGlobalDebug = newConfig.debugMode === true;
349
+ const hostname = typeof window !== "undefined" ? window.location.hostname : "";
350
+ const envs = newConfig.environments;
351
+ const matchedEnv = hostname ? envs == null ? void 0 : envs.find((e) => {
352
+ if (typeof e.url !== "string" || e.url === "") return false;
353
+ let storedHost;
354
+ try {
355
+ storedHost = e.url.includes("://") ? new URL(e.url).hostname : e.url;
356
+ } catch {
357
+ storedHost = e.url;
358
+ }
359
+ return storedHost === hostname;
360
+ }) : void 0;
361
+ const isEnvDebug = (matchedEnv == null ? void 0 : matchedEnv.debugEnabled) === true;
362
+ const shouldDebug = isGlobalDebug || isEnvDebug;
363
+ if (shouldDebug && !this.debugOverlayEl) {
364
+ if (isEnvDebug && !isGlobalDebug) {
365
+ this.debugLog(
366
+ "info",
367
+ `[env] matched environment "${matchedEnv.name}" (${hostname}) \u2014 enabling debug overlay`
368
+ );
369
+ }
330
370
  this.setupDebugOverlay();
371
+ } else if (!shouldDebug && this.debugOverlayEl) {
372
+ this.debugLog("info", "[debug] debug disabled by config \u2014 removing overlay");
373
+ this.stopDebugOverlay();
331
374
  }
332
- if (!this.debugOverlayEl) {
333
- const hostname = typeof window !== "undefined" ? window.location.hostname : "";
334
- const envs = newConfig.environments;
335
- const matchedEnv = hostname ? envs == null ? void 0 : envs.find((e) => typeof e.url === "string" && e.url !== "" && e.url === hostname) : void 0;
336
- if ((matchedEnv == null ? void 0 : matchedEnv.debugEnabled) === true) {
337
- this.debugLog("info", `[env] matched environment "${matchedEnv.name}" (${hostname})`);
338
- this.setupDebugOverlay();
375
+ if (matchedEnv && matchedEnv.analyticsEnabled === false) {
376
+ if (this.config.enableAnalytics) {
377
+ this.debugLog(
378
+ "info",
379
+ `[env] analytics disabled for environment "${matchedEnv.name}" (${hostname}) \u2014 stopping analytics`
380
+ );
381
+ this.stopAnalyticsFlushing();
382
+ }
383
+ } else if (matchedEnv && matchedEnv.analyticsEnabled !== false && this.config.enableAnalytics) {
384
+ if (!this.analyticsFlushIntervalId) {
385
+ this.debugLog(
386
+ "info",
387
+ `[env] analytics re-enabled for environment "${matchedEnv.name}" (${hostname})`
388
+ );
389
+ this.startAnalyticsFlushing();
339
390
  }
340
391
  }
341
392
  if (newConfig.maintenance.enabled && !wasMaintenanceEnabled) {
@@ -357,7 +408,10 @@ var _SiteManager = class _SiteManager {
357
408
  } catch (error) {
358
409
  this.consecutiveFetchFailures++;
359
410
  const errMsg = error instanceof Error ? error.message : String(error);
360
- this.debugLog("error", `[config] fetch failed (attempt ${this.consecutiveFetchFailures}): ${errMsg}`);
411
+ this.debugLog(
412
+ "error",
413
+ `[config] fetch failed (attempt ${this.consecutiveFetchFailures}): ${errMsg}`
414
+ );
361
415
  this.errorCount++;
362
416
  if (this.consecutiveFetchFailures <= _SiteManager.MAX_CONSECUTIVE_FAILURES) {
363
417
  const isNetworkError = error instanceof TypeError && (error.message.includes("NetworkError") || error.message.includes("Failed to fetch") || error.message.includes("fetch"));
@@ -376,7 +430,10 @@ var _SiteManager = class _SiteManager {
376
430
  } else if (this.consecutiveFetchFailures === _SiteManager.MAX_CONSECUTIVE_FAILURES + 1) {
377
431
  const silenceMsg = `Featurely Site Manager: Silencing repeated fetch errors after ${_SiteManager.MAX_CONSECUTIVE_FAILURES} failures. Check your apiUrl and Content-Security-Policy configuration.`;
378
432
  console.error(silenceMsg);
379
- this.debugLog("error", `[config] silencing errors after ${_SiteManager.MAX_CONSECUTIVE_FAILURES} consecutive failures`);
433
+ this.debugLog(
434
+ "error",
435
+ `[config] silencing errors after ${_SiteManager.MAX_CONSECUTIVE_FAILURES} consecutive failures`
436
+ );
380
437
  }
381
438
  }
382
439
  }
@@ -415,7 +472,10 @@ var _SiteManager = class _SiteManager {
415
472
  const eventsToSend = [...this.analyticsQueue];
416
473
  this.analyticsQueue = [];
417
474
  this.analyticsEventsSent += eventsToSend.length;
418
- this.debugLog("info", `[analytics] flushing ${eventsToSend.length} event(s) (total sent: ${this.analyticsEventsSent})`);
475
+ this.debugLog(
476
+ "info",
477
+ `[analytics] flushing ${eventsToSend.length} event(s) (total sent: ${this.analyticsEventsSent})`
478
+ );
419
479
  for (const event of eventsToSend) {
420
480
  const payload = JSON.stringify({
421
481
  eventName: event.eventName,
@@ -439,7 +499,11 @@ var _SiteManager = class _SiteManager {
439
499
  body: payload,
440
500
  keepalive: true
441
501
  });
442
- this.debugNetwork(`/api/projects/${this.config.projectId}/analytics/events`, res.status, Date.now() - t0);
502
+ this.debugNetwork(
503
+ `/api/projects/${this.config.projectId}/analytics/events`,
504
+ res.status,
505
+ Date.now() - t0
506
+ );
443
507
  if (!res.ok) {
444
508
  const body = await res.text().catch(() => "(unreadable)");
445
509
  const errDetail = `[analytics] event "${event.eventName}" rejected: ${res.status} ${res.statusText} \u2014 ${body}`;
@@ -451,11 +515,20 @@ var _SiteManager = class _SiteManager {
451
515
  const sentViaBeacon = typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && navigator.sendBeacon(url, payload);
452
516
  const errMsg = fetchError instanceof Error ? fetchError.message : String(fetchError);
453
517
  if (!sentViaBeacon) {
454
- console.error(`[Featurely] Failed to send analytics event "${event.eventName}":`, fetchError);
455
- this.debugLog("error", `[analytics] send failed for "${event.eventName}": ${errMsg}`);
518
+ console.error(
519
+ `[Featurely] Failed to send analytics event "${event.eventName}":`,
520
+ fetchError
521
+ );
522
+ this.debugLog(
523
+ "error",
524
+ `[analytics] send failed for "${event.eventName}": ${errMsg}`
525
+ );
456
526
  this.errorCount++;
457
527
  } else {
458
- this.debugLog("warn", `[analytics] fetch failed for "${event.eventName}" (${errMsg}), delivered via sendBeacon`);
528
+ this.debugLog(
529
+ "warn",
530
+ `[analytics] fetch failed for "${event.eventName}" (${errMsg}), delivered via sendBeacon`
531
+ );
459
532
  }
460
533
  }
461
534
  }
@@ -502,14 +575,18 @@ var _SiteManager = class _SiteManager {
502
575
  this.originalConsoleError = console.error.bind(console);
503
576
  console.error = (...args) => {
504
577
  this.originalConsoleError(...args);
505
- const msg = args.map((a) => typeof a === "string" ? a : a instanceof Error ? `${a.message}` : JSON.stringify(a)).join(" ");
578
+ const msg = args.map(
579
+ (a) => typeof a === "string" ? a : a instanceof Error ? `${a.message}` : JSON.stringify(a)
580
+ ).join(" ");
506
581
  this.errorCount++;
507
582
  this.debugLog("error", `[console.error] ${msg.slice(0, 300)}`);
508
583
  };
509
584
  this.originalConsoleWarn = console.warn.bind(console);
510
585
  console.warn = (...args) => {
511
586
  this.originalConsoleWarn(...args);
512
- const msg = args.map((a) => typeof a === "string" ? a : a instanceof Error ? `${a.message}` : JSON.stringify(a)).join(" ");
587
+ const msg = args.map(
588
+ (a) => typeof a === "string" ? a : a instanceof Error ? `${a.message}` : JSON.stringify(a)
589
+ ).join(" ");
513
590
  this.debugLog("warn", `[console.warn] ${msg.slice(0, 300)}`);
514
591
  };
515
592
  window.addEventListener("error", (e) => {
@@ -562,7 +639,7 @@ var _SiteManager = class _SiteManager {
562
639
  this.debugOverlayEl = el;
563
640
  this.setupGlobalErrorCapture();
564
641
  this.debugLog("info", `[site-manager] debug overlay initialized`);
565
- this.debugLog("info", `[site-manager] v1.1.14 | project: ${this.config.projectId}`);
642
+ this.debugLog("info", `[site-manager] v1.1.19 | project: ${this.config.projectId}`);
566
643
  this.renderDebugOverlay();
567
644
  this.debugRefreshId = setInterval(() => this.renderDebugOverlay(), 1500);
568
645
  }
@@ -744,7 +821,10 @@ var _SiteManager = class _SiteManager {
744
821
  };
745
822
  switch (action) {
746
823
  case "test-event":
747
- this.trackEvent("debug_test_event", { source: "debug_panel", ts: (/* @__PURE__ */ new Date()).toISOString() });
824
+ this.trackEvent("debug_test_event", {
825
+ source: "debug_panel",
826
+ ts: (/* @__PURE__ */ new Date()).toISOString()
827
+ });
748
828
  feedback("debug_test_event queued", true);
749
829
  break;
750
830
  case "flush": {
@@ -786,7 +866,10 @@ var _SiteManager = class _SiteManager {
786
866
  case "copy-logs": {
787
867
  const text = this.debugLogs.slice().reverse().map((l) => `[${l.ts}] [${l.level.toUpperCase()}] ${l.msg}`).join("\n");
788
868
  void navigator.clipboard.writeText(text).then(() => {
789
- feedback(`Copied ${this.debugLogs.length} log line${this.debugLogs.length !== 1 ? "s" : ""}`, true);
869
+ feedback(
870
+ `Copied ${this.debugLogs.length} log line${this.debugLogs.length !== 1 ? "s" : ""}`,
871
+ true
872
+ );
790
873
  }).catch(() => {
791
874
  feedback("Copy failed \u2014 clipboard not available", false);
792
875
  });
@@ -904,9 +987,7 @@ var _SiteManager = class _SiteManager {
904
987
  async checkVersion(currentVersion) {
905
988
  const versionToCheck = currentVersion || this.config.appVersion;
906
989
  if (!versionToCheck) {
907
- console.warn(
908
- "Featurely Site Manager: appVersion not provided for version check"
909
- );
990
+ console.warn("Featurely Site Manager: appVersion not provided for version check");
910
991
  return null;
911
992
  }
912
993
  try {
@@ -1050,7 +1131,10 @@ var _SiteManager = class _SiteManager {
1050
1131
  this.debugLog("info", "[maintenance] ENABLED \u2014 user bypassed (whitelist match)");
1051
1132
  return;
1052
1133
  }
1053
- this.debugLog("warn", `[maintenance] ENABLED (type: ${this.siteConfig.maintenance.type})`);
1134
+ this.debugLog(
1135
+ "warn",
1136
+ `[maintenance] ENABLED (type: ${this.siteConfig.maintenance.type})`
1137
+ );
1054
1138
  this.showMaintenancePage();
1055
1139
  this.trackEvent("maintenance_enabled", {
1056
1140
  maintenanceType: this.siteConfig.maintenance.type
@@ -1122,7 +1206,17 @@ var _SiteManager = class _SiteManager {
1122
1206
  "button"
1123
1207
  ],
1124
1208
  ALLOWED_ATTR: ["class", "id", "href", "src", "alt", "title", "style"],
1125
- FORBID_TAGS: ["script", "iframe", "embed", "object", "applet", "meta", "link", "form", "input"],
1209
+ FORBID_TAGS: [
1210
+ "script",
1211
+ "iframe",
1212
+ "embed",
1213
+ "object",
1214
+ "applet",
1215
+ "meta",
1216
+ "link",
1217
+ "form",
1218
+ "input"
1219
+ ],
1126
1220
  FORBID_ATTR: ["onerror", "onload", "onclick", "onmouseover"],
1127
1221
  ALLOW_DATA_ATTR: false
1128
1222
  });
@@ -1190,10 +1284,16 @@ var _SiteManager = class _SiteManager {
1190
1284
  this.messageContainers.delete(id);
1191
1285
  }
1192
1286
  });
1193
- this.debugLog("info", `[messages] ${activeMessages.length} active (${(_b = (_a = this.siteConfig) == null ? void 0 : _a.messages.length) != null ? _b : 0} total)`);
1287
+ this.debugLog(
1288
+ "info",
1289
+ `[messages] ${activeMessages.length} active (${(_b = (_a = this.siteConfig) == null ? void 0 : _a.messages.length) != null ? _b : 0} total)`
1290
+ );
1194
1291
  activeMessages.forEach((message) => {
1195
1292
  if (!this.messageContainers.has(message.id)) {
1196
- this.debugLog("info", `[messages] showing "${message.title}" (${message.type}, ${message.style})`);
1293
+ this.debugLog(
1294
+ "info",
1295
+ `[messages] showing "${message.title}" (${message.type}, ${message.style})`
1296
+ );
1197
1297
  this.showMessage(message);
1198
1298
  if (this.config.onMessageReceived) {
1199
1299
  this.config.onMessageReceived(message);
@@ -1459,7 +1559,8 @@ var _SiteManager = class _SiteManager {
1459
1559
 
1460
1560
  .featurely-message-body {
1461
1561
  font-size: 14px;
1462
- opacity: 0.9;
1562
+ /* opacity < 1 reduces effective contrast; inherit full color from parent */
1563
+ opacity: 1;
1463
1564
  }
1464
1565
 
1465
1566
  .featurely-message-cta {
@@ -1510,16 +1611,17 @@ var _SiteManager = class _SiteManager {
1510
1611
 
1511
1612
  .featurely-message-warning {
1512
1613
  background: #fff3e0;
1513
- color: #f57c00;
1614
+ /* #803600 on #fff3e0 \u2248 7.8:1 contrast \u2014 WCAG AA compliant */
1615
+ color: #803600;
1514
1616
  }
1515
1617
 
1516
1618
  .featurely-message-warning .featurely-message-cta {
1517
- background: #f57c00;
1619
+ background: #803600;
1518
1620
  color: white;
1519
1621
  }
1520
1622
 
1521
1623
  .featurely-message-warning .featurely-message-cta:hover {
1522
- background: #ef6c00;
1624
+ background: #6b2d00;
1523
1625
  }
1524
1626
 
1525
1627
  .featurely-message-error {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "featurely-site-manager",
3
- "version": "1.1.18",
3
+ "version": "1.1.20",
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",
@@ -50,10 +50,13 @@
50
50
  "homepage": "https://featurely.no",
51
51
  "devDependencies": {
52
52
  "@types/dompurify": "^3.0.5",
53
+ "drizzle-kit": "^0.31.10",
53
54
  "tsup": "^8.5.1",
54
55
  "typescript": "^5.0.0"
55
56
  },
56
57
  "dependencies": {
57
- "dompurify": "^3.3.3"
58
+ "@neondatabase/serverless": "^1.0.2",
59
+ "dompurify": "^3.3.3",
60
+ "drizzle-orm": "^0.45.2"
58
61
  }
59
62
  }