featurely-site-manager 1.1.18 → 1.1.19
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 +2 -1
- package/README.md +41 -37
- package/dist/index.js +126 -33
- package/dist/index.mjs +126 -33
- package/package.json +5 -2
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
|
-
-
|
|
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 (
|
|
183
|
-
|
|
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
|
|
259
|
-
|
|
|
260
|
-
| `session_start` | On first page load
|
|
261
|
-
| `page_view`
|
|
262
|
-
| `page_exit`
|
|
263
|
-
| `user_login`
|
|
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
|
|
286
|
-
|
|
|
287
|
-
| **SDK**
|
|
288
|
-
| **Logs**
|
|
289
|
-
| **Network** | Config fetch log with status codes and timestamps
|
|
290
|
-
| **Events**
|
|
291
|
-
| **Test**
|
|
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
|
|
336
|
-
| ------------------------ | --------------- | -------- |
|
|
337
|
-
| `apiKey` | `string` | ✅ | -
|
|
338
|
-
| `projectId` | `string` | ✅ | -
|
|
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`
|
|
341
|
-
| `userEmail` | `string` | ❌ | -
|
|
342
|
-
| `userId` | `string` | ❌ | -
|
|
343
|
-
| `bypassCheck` | `() => boolean` | ❌ | -
|
|
344
|
-
| `onMaintenanceEnabled` | `function` | ❌ | -
|
|
345
|
-
| `onMaintenanceDisabled` | `function` | ❌ | -
|
|
346
|
-
| `onMessageReceived` | `function` | ❌ | -
|
|
347
|
-
| `onMessageDismissed` | `function` | ❌ | -
|
|
348
|
-
| `onFeatureFlagsUpdated` | `function` | ❌ | -
|
|
349
|
-
| `enableAnalytics` | `boolean` | ❌ | `true`
|
|
350
|
-
| `analyticsFlushInterval` | `number` | ❌ | `60000`
|
|
351
|
-
| `appVersion` | `string` | ❌ | -
|
|
352
|
-
| `enableVersionCheck` | `boolean` | ❌ | `false`
|
|
353
|
-
| `versionCheckInterval` | `number` | ❌ | `3600000`
|
|
354
|
-
| `onUpdateAvailable` | `function` | ❌ | -
|
|
355
|
-
| `onUpdateRequired` | `function` | ❌ | -
|
|
356
|
-
| `onError` | `function` | ❌ | -
|
|
357
|
-
| `debugMode` | `boolean` | ❌ | `false`
|
|
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(
|
|
119
|
-
|
|
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(
|
|
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({
|
|
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(
|
|
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,48 @@ 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(
|
|
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
|
-
|
|
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(
|
|
387
|
+
(e) => typeof e.url === "string" && e.url !== "" && e.url === hostname
|
|
388
|
+
) : void 0;
|
|
389
|
+
const isEnvDebug = (matchedEnv == null ? void 0 : matchedEnv.debugEnabled) === true;
|
|
390
|
+
const shouldDebug = isGlobalDebug || isEnvDebug;
|
|
391
|
+
if (shouldDebug && !this.debugOverlayEl) {
|
|
392
|
+
if (isEnvDebug && !isGlobalDebug) {
|
|
393
|
+
this.debugLog(
|
|
394
|
+
"info",
|
|
395
|
+
`[env] matched environment "${matchedEnv.name}" (${hostname}) \u2014 enabling debug overlay`
|
|
396
|
+
);
|
|
397
|
+
}
|
|
365
398
|
this.setupDebugOverlay();
|
|
399
|
+
} else if (!shouldDebug && this.debugOverlayEl) {
|
|
400
|
+
this.debugLog("info", "[debug] debug disabled by config \u2014 removing overlay");
|
|
401
|
+
this.stopDebugOverlay();
|
|
366
402
|
}
|
|
367
|
-
if (
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
this.
|
|
403
|
+
if (matchedEnv && matchedEnv.analyticsEnabled === false) {
|
|
404
|
+
if (this.config.enableAnalytics) {
|
|
405
|
+
this.debugLog(
|
|
406
|
+
"info",
|
|
407
|
+
`[env] analytics disabled for environment "${matchedEnv.name}" (${hostname}) \u2014 stopping analytics`
|
|
408
|
+
);
|
|
409
|
+
this.stopAnalyticsFlushing();
|
|
410
|
+
}
|
|
411
|
+
} else if (matchedEnv && matchedEnv.analyticsEnabled !== false && this.config.enableAnalytics) {
|
|
412
|
+
if (!this.analyticsFlushIntervalId) {
|
|
413
|
+
this.debugLog(
|
|
414
|
+
"info",
|
|
415
|
+
`[env] analytics re-enabled for environment "${matchedEnv.name}" (${hostname})`
|
|
416
|
+
);
|
|
417
|
+
this.startAnalyticsFlushing();
|
|
374
418
|
}
|
|
375
419
|
}
|
|
376
420
|
if (newConfig.maintenance.enabled && !wasMaintenanceEnabled) {
|
|
@@ -392,7 +436,10 @@ var _SiteManager = class _SiteManager {
|
|
|
392
436
|
} catch (error) {
|
|
393
437
|
this.consecutiveFetchFailures++;
|
|
394
438
|
const errMsg = error instanceof Error ? error.message : String(error);
|
|
395
|
-
this.debugLog(
|
|
439
|
+
this.debugLog(
|
|
440
|
+
"error",
|
|
441
|
+
`[config] fetch failed (attempt ${this.consecutiveFetchFailures}): ${errMsg}`
|
|
442
|
+
);
|
|
396
443
|
this.errorCount++;
|
|
397
444
|
if (this.consecutiveFetchFailures <= _SiteManager.MAX_CONSECUTIVE_FAILURES) {
|
|
398
445
|
const isNetworkError = error instanceof TypeError && (error.message.includes("NetworkError") || error.message.includes("Failed to fetch") || error.message.includes("fetch"));
|
|
@@ -411,7 +458,10 @@ var _SiteManager = class _SiteManager {
|
|
|
411
458
|
} else if (this.consecutiveFetchFailures === _SiteManager.MAX_CONSECUTIVE_FAILURES + 1) {
|
|
412
459
|
const silenceMsg = `Featurely Site Manager: Silencing repeated fetch errors after ${_SiteManager.MAX_CONSECUTIVE_FAILURES} failures. Check your apiUrl and Content-Security-Policy configuration.`;
|
|
413
460
|
console.error(silenceMsg);
|
|
414
|
-
this.debugLog(
|
|
461
|
+
this.debugLog(
|
|
462
|
+
"error",
|
|
463
|
+
`[config] silencing errors after ${_SiteManager.MAX_CONSECUTIVE_FAILURES} consecutive failures`
|
|
464
|
+
);
|
|
415
465
|
}
|
|
416
466
|
}
|
|
417
467
|
}
|
|
@@ -450,7 +500,10 @@ var _SiteManager = class _SiteManager {
|
|
|
450
500
|
const eventsToSend = [...this.analyticsQueue];
|
|
451
501
|
this.analyticsQueue = [];
|
|
452
502
|
this.analyticsEventsSent += eventsToSend.length;
|
|
453
|
-
this.debugLog(
|
|
503
|
+
this.debugLog(
|
|
504
|
+
"info",
|
|
505
|
+
`[analytics] flushing ${eventsToSend.length} event(s) (total sent: ${this.analyticsEventsSent})`
|
|
506
|
+
);
|
|
454
507
|
for (const event of eventsToSend) {
|
|
455
508
|
const payload = JSON.stringify({
|
|
456
509
|
eventName: event.eventName,
|
|
@@ -474,7 +527,11 @@ var _SiteManager = class _SiteManager {
|
|
|
474
527
|
body: payload,
|
|
475
528
|
keepalive: true
|
|
476
529
|
});
|
|
477
|
-
this.debugNetwork(
|
|
530
|
+
this.debugNetwork(
|
|
531
|
+
`/api/projects/${this.config.projectId}/analytics/events`,
|
|
532
|
+
res.status,
|
|
533
|
+
Date.now() - t0
|
|
534
|
+
);
|
|
478
535
|
if (!res.ok) {
|
|
479
536
|
const body = await res.text().catch(() => "(unreadable)");
|
|
480
537
|
const errDetail = `[analytics] event "${event.eventName}" rejected: ${res.status} ${res.statusText} \u2014 ${body}`;
|
|
@@ -486,11 +543,20 @@ var _SiteManager = class _SiteManager {
|
|
|
486
543
|
const sentViaBeacon = typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && navigator.sendBeacon(url, payload);
|
|
487
544
|
const errMsg = fetchError instanceof Error ? fetchError.message : String(fetchError);
|
|
488
545
|
if (!sentViaBeacon) {
|
|
489
|
-
console.error(
|
|
490
|
-
|
|
546
|
+
console.error(
|
|
547
|
+
`[Featurely] Failed to send analytics event "${event.eventName}":`,
|
|
548
|
+
fetchError
|
|
549
|
+
);
|
|
550
|
+
this.debugLog(
|
|
551
|
+
"error",
|
|
552
|
+
`[analytics] send failed for "${event.eventName}": ${errMsg}`
|
|
553
|
+
);
|
|
491
554
|
this.errorCount++;
|
|
492
555
|
} else {
|
|
493
|
-
this.debugLog(
|
|
556
|
+
this.debugLog(
|
|
557
|
+
"warn",
|
|
558
|
+
`[analytics] fetch failed for "${event.eventName}" (${errMsg}), delivered via sendBeacon`
|
|
559
|
+
);
|
|
494
560
|
}
|
|
495
561
|
}
|
|
496
562
|
}
|
|
@@ -537,14 +603,18 @@ var _SiteManager = class _SiteManager {
|
|
|
537
603
|
this.originalConsoleError = console.error.bind(console);
|
|
538
604
|
console.error = (...args) => {
|
|
539
605
|
this.originalConsoleError(...args);
|
|
540
|
-
const msg = args.map(
|
|
606
|
+
const msg = args.map(
|
|
607
|
+
(a) => typeof a === "string" ? a : a instanceof Error ? `${a.message}` : JSON.stringify(a)
|
|
608
|
+
).join(" ");
|
|
541
609
|
this.errorCount++;
|
|
542
610
|
this.debugLog("error", `[console.error] ${msg.slice(0, 300)}`);
|
|
543
611
|
};
|
|
544
612
|
this.originalConsoleWarn = console.warn.bind(console);
|
|
545
613
|
console.warn = (...args) => {
|
|
546
614
|
this.originalConsoleWarn(...args);
|
|
547
|
-
const msg = args.map(
|
|
615
|
+
const msg = args.map(
|
|
616
|
+
(a) => typeof a === "string" ? a : a instanceof Error ? `${a.message}` : JSON.stringify(a)
|
|
617
|
+
).join(" ");
|
|
548
618
|
this.debugLog("warn", `[console.warn] ${msg.slice(0, 300)}`);
|
|
549
619
|
};
|
|
550
620
|
window.addEventListener("error", (e) => {
|
|
@@ -597,7 +667,7 @@ var _SiteManager = class _SiteManager {
|
|
|
597
667
|
this.debugOverlayEl = el;
|
|
598
668
|
this.setupGlobalErrorCapture();
|
|
599
669
|
this.debugLog("info", `[site-manager] debug overlay initialized`);
|
|
600
|
-
this.debugLog("info", `[site-manager] v1.1.
|
|
670
|
+
this.debugLog("info", `[site-manager] v1.1.19 | project: ${this.config.projectId}`);
|
|
601
671
|
this.renderDebugOverlay();
|
|
602
672
|
this.debugRefreshId = setInterval(() => this.renderDebugOverlay(), 1500);
|
|
603
673
|
}
|
|
@@ -779,7 +849,10 @@ var _SiteManager = class _SiteManager {
|
|
|
779
849
|
};
|
|
780
850
|
switch (action) {
|
|
781
851
|
case "test-event":
|
|
782
|
-
this.trackEvent("debug_test_event", {
|
|
852
|
+
this.trackEvent("debug_test_event", {
|
|
853
|
+
source: "debug_panel",
|
|
854
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
855
|
+
});
|
|
783
856
|
feedback("debug_test_event queued", true);
|
|
784
857
|
break;
|
|
785
858
|
case "flush": {
|
|
@@ -821,7 +894,10 @@ var _SiteManager = class _SiteManager {
|
|
|
821
894
|
case "copy-logs": {
|
|
822
895
|
const text = this.debugLogs.slice().reverse().map((l) => `[${l.ts}] [${l.level.toUpperCase()}] ${l.msg}`).join("\n");
|
|
823
896
|
void navigator.clipboard.writeText(text).then(() => {
|
|
824
|
-
feedback(
|
|
897
|
+
feedback(
|
|
898
|
+
`Copied ${this.debugLogs.length} log line${this.debugLogs.length !== 1 ? "s" : ""}`,
|
|
899
|
+
true
|
|
900
|
+
);
|
|
825
901
|
}).catch(() => {
|
|
826
902
|
feedback("Copy failed \u2014 clipboard not available", false);
|
|
827
903
|
});
|
|
@@ -939,9 +1015,7 @@ var _SiteManager = class _SiteManager {
|
|
|
939
1015
|
async checkVersion(currentVersion) {
|
|
940
1016
|
const versionToCheck = currentVersion || this.config.appVersion;
|
|
941
1017
|
if (!versionToCheck) {
|
|
942
|
-
console.warn(
|
|
943
|
-
"Featurely Site Manager: appVersion not provided for version check"
|
|
944
|
-
);
|
|
1018
|
+
console.warn("Featurely Site Manager: appVersion not provided for version check");
|
|
945
1019
|
return null;
|
|
946
1020
|
}
|
|
947
1021
|
try {
|
|
@@ -1085,7 +1159,10 @@ var _SiteManager = class _SiteManager {
|
|
|
1085
1159
|
this.debugLog("info", "[maintenance] ENABLED \u2014 user bypassed (whitelist match)");
|
|
1086
1160
|
return;
|
|
1087
1161
|
}
|
|
1088
|
-
this.debugLog(
|
|
1162
|
+
this.debugLog(
|
|
1163
|
+
"warn",
|
|
1164
|
+
`[maintenance] ENABLED (type: ${this.siteConfig.maintenance.type})`
|
|
1165
|
+
);
|
|
1089
1166
|
this.showMaintenancePage();
|
|
1090
1167
|
this.trackEvent("maintenance_enabled", {
|
|
1091
1168
|
maintenanceType: this.siteConfig.maintenance.type
|
|
@@ -1157,7 +1234,17 @@ var _SiteManager = class _SiteManager {
|
|
|
1157
1234
|
"button"
|
|
1158
1235
|
],
|
|
1159
1236
|
ALLOWED_ATTR: ["class", "id", "href", "src", "alt", "title", "style"],
|
|
1160
|
-
FORBID_TAGS: [
|
|
1237
|
+
FORBID_TAGS: [
|
|
1238
|
+
"script",
|
|
1239
|
+
"iframe",
|
|
1240
|
+
"embed",
|
|
1241
|
+
"object",
|
|
1242
|
+
"applet",
|
|
1243
|
+
"meta",
|
|
1244
|
+
"link",
|
|
1245
|
+
"form",
|
|
1246
|
+
"input"
|
|
1247
|
+
],
|
|
1161
1248
|
FORBID_ATTR: ["onerror", "onload", "onclick", "onmouseover"],
|
|
1162
1249
|
ALLOW_DATA_ATTR: false
|
|
1163
1250
|
});
|
|
@@ -1225,10 +1312,16 @@ var _SiteManager = class _SiteManager {
|
|
|
1225
1312
|
this.messageContainers.delete(id);
|
|
1226
1313
|
}
|
|
1227
1314
|
});
|
|
1228
|
-
this.debugLog(
|
|
1315
|
+
this.debugLog(
|
|
1316
|
+
"info",
|
|
1317
|
+
`[messages] ${activeMessages.length} active (${(_b = (_a = this.siteConfig) == null ? void 0 : _a.messages.length) != null ? _b : 0} total)`
|
|
1318
|
+
);
|
|
1229
1319
|
activeMessages.forEach((message) => {
|
|
1230
1320
|
if (!this.messageContainers.has(message.id)) {
|
|
1231
|
-
this.debugLog(
|
|
1321
|
+
this.debugLog(
|
|
1322
|
+
"info",
|
|
1323
|
+
`[messages] showing "${message.title}" (${message.type}, ${message.style})`
|
|
1324
|
+
);
|
|
1232
1325
|
this.showMessage(message);
|
|
1233
1326
|
if (this.config.onMessageReceived) {
|
|
1234
1327
|
this.config.onMessageReceived(message);
|
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(
|
|
84
|
-
|
|
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(
|
|
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({
|
|
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(
|
|
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,48 @@ 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(
|
|
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
|
-
|
|
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(
|
|
352
|
+
(e) => typeof e.url === "string" && e.url !== "" && e.url === hostname
|
|
353
|
+
) : void 0;
|
|
354
|
+
const isEnvDebug = (matchedEnv == null ? void 0 : matchedEnv.debugEnabled) === true;
|
|
355
|
+
const shouldDebug = isGlobalDebug || isEnvDebug;
|
|
356
|
+
if (shouldDebug && !this.debugOverlayEl) {
|
|
357
|
+
if (isEnvDebug && !isGlobalDebug) {
|
|
358
|
+
this.debugLog(
|
|
359
|
+
"info",
|
|
360
|
+
`[env] matched environment "${matchedEnv.name}" (${hostname}) \u2014 enabling debug overlay`
|
|
361
|
+
);
|
|
362
|
+
}
|
|
330
363
|
this.setupDebugOverlay();
|
|
364
|
+
} else if (!shouldDebug && this.debugOverlayEl) {
|
|
365
|
+
this.debugLog("info", "[debug] debug disabled by config \u2014 removing overlay");
|
|
366
|
+
this.stopDebugOverlay();
|
|
331
367
|
}
|
|
332
|
-
if (
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
this.
|
|
368
|
+
if (matchedEnv && matchedEnv.analyticsEnabled === false) {
|
|
369
|
+
if (this.config.enableAnalytics) {
|
|
370
|
+
this.debugLog(
|
|
371
|
+
"info",
|
|
372
|
+
`[env] analytics disabled for environment "${matchedEnv.name}" (${hostname}) \u2014 stopping analytics`
|
|
373
|
+
);
|
|
374
|
+
this.stopAnalyticsFlushing();
|
|
375
|
+
}
|
|
376
|
+
} else if (matchedEnv && matchedEnv.analyticsEnabled !== false && this.config.enableAnalytics) {
|
|
377
|
+
if (!this.analyticsFlushIntervalId) {
|
|
378
|
+
this.debugLog(
|
|
379
|
+
"info",
|
|
380
|
+
`[env] analytics re-enabled for environment "${matchedEnv.name}" (${hostname})`
|
|
381
|
+
);
|
|
382
|
+
this.startAnalyticsFlushing();
|
|
339
383
|
}
|
|
340
384
|
}
|
|
341
385
|
if (newConfig.maintenance.enabled && !wasMaintenanceEnabled) {
|
|
@@ -357,7 +401,10 @@ var _SiteManager = class _SiteManager {
|
|
|
357
401
|
} catch (error) {
|
|
358
402
|
this.consecutiveFetchFailures++;
|
|
359
403
|
const errMsg = error instanceof Error ? error.message : String(error);
|
|
360
|
-
this.debugLog(
|
|
404
|
+
this.debugLog(
|
|
405
|
+
"error",
|
|
406
|
+
`[config] fetch failed (attempt ${this.consecutiveFetchFailures}): ${errMsg}`
|
|
407
|
+
);
|
|
361
408
|
this.errorCount++;
|
|
362
409
|
if (this.consecutiveFetchFailures <= _SiteManager.MAX_CONSECUTIVE_FAILURES) {
|
|
363
410
|
const isNetworkError = error instanceof TypeError && (error.message.includes("NetworkError") || error.message.includes("Failed to fetch") || error.message.includes("fetch"));
|
|
@@ -376,7 +423,10 @@ var _SiteManager = class _SiteManager {
|
|
|
376
423
|
} else if (this.consecutiveFetchFailures === _SiteManager.MAX_CONSECUTIVE_FAILURES + 1) {
|
|
377
424
|
const silenceMsg = `Featurely Site Manager: Silencing repeated fetch errors after ${_SiteManager.MAX_CONSECUTIVE_FAILURES} failures. Check your apiUrl and Content-Security-Policy configuration.`;
|
|
378
425
|
console.error(silenceMsg);
|
|
379
|
-
this.debugLog(
|
|
426
|
+
this.debugLog(
|
|
427
|
+
"error",
|
|
428
|
+
`[config] silencing errors after ${_SiteManager.MAX_CONSECUTIVE_FAILURES} consecutive failures`
|
|
429
|
+
);
|
|
380
430
|
}
|
|
381
431
|
}
|
|
382
432
|
}
|
|
@@ -415,7 +465,10 @@ var _SiteManager = class _SiteManager {
|
|
|
415
465
|
const eventsToSend = [...this.analyticsQueue];
|
|
416
466
|
this.analyticsQueue = [];
|
|
417
467
|
this.analyticsEventsSent += eventsToSend.length;
|
|
418
|
-
this.debugLog(
|
|
468
|
+
this.debugLog(
|
|
469
|
+
"info",
|
|
470
|
+
`[analytics] flushing ${eventsToSend.length} event(s) (total sent: ${this.analyticsEventsSent})`
|
|
471
|
+
);
|
|
419
472
|
for (const event of eventsToSend) {
|
|
420
473
|
const payload = JSON.stringify({
|
|
421
474
|
eventName: event.eventName,
|
|
@@ -439,7 +492,11 @@ var _SiteManager = class _SiteManager {
|
|
|
439
492
|
body: payload,
|
|
440
493
|
keepalive: true
|
|
441
494
|
});
|
|
442
|
-
this.debugNetwork(
|
|
495
|
+
this.debugNetwork(
|
|
496
|
+
`/api/projects/${this.config.projectId}/analytics/events`,
|
|
497
|
+
res.status,
|
|
498
|
+
Date.now() - t0
|
|
499
|
+
);
|
|
443
500
|
if (!res.ok) {
|
|
444
501
|
const body = await res.text().catch(() => "(unreadable)");
|
|
445
502
|
const errDetail = `[analytics] event "${event.eventName}" rejected: ${res.status} ${res.statusText} \u2014 ${body}`;
|
|
@@ -451,11 +508,20 @@ var _SiteManager = class _SiteManager {
|
|
|
451
508
|
const sentViaBeacon = typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && navigator.sendBeacon(url, payload);
|
|
452
509
|
const errMsg = fetchError instanceof Error ? fetchError.message : String(fetchError);
|
|
453
510
|
if (!sentViaBeacon) {
|
|
454
|
-
console.error(
|
|
455
|
-
|
|
511
|
+
console.error(
|
|
512
|
+
`[Featurely] Failed to send analytics event "${event.eventName}":`,
|
|
513
|
+
fetchError
|
|
514
|
+
);
|
|
515
|
+
this.debugLog(
|
|
516
|
+
"error",
|
|
517
|
+
`[analytics] send failed for "${event.eventName}": ${errMsg}`
|
|
518
|
+
);
|
|
456
519
|
this.errorCount++;
|
|
457
520
|
} else {
|
|
458
|
-
this.debugLog(
|
|
521
|
+
this.debugLog(
|
|
522
|
+
"warn",
|
|
523
|
+
`[analytics] fetch failed for "${event.eventName}" (${errMsg}), delivered via sendBeacon`
|
|
524
|
+
);
|
|
459
525
|
}
|
|
460
526
|
}
|
|
461
527
|
}
|
|
@@ -502,14 +568,18 @@ var _SiteManager = class _SiteManager {
|
|
|
502
568
|
this.originalConsoleError = console.error.bind(console);
|
|
503
569
|
console.error = (...args) => {
|
|
504
570
|
this.originalConsoleError(...args);
|
|
505
|
-
const msg = args.map(
|
|
571
|
+
const msg = args.map(
|
|
572
|
+
(a) => typeof a === "string" ? a : a instanceof Error ? `${a.message}` : JSON.stringify(a)
|
|
573
|
+
).join(" ");
|
|
506
574
|
this.errorCount++;
|
|
507
575
|
this.debugLog("error", `[console.error] ${msg.slice(0, 300)}`);
|
|
508
576
|
};
|
|
509
577
|
this.originalConsoleWarn = console.warn.bind(console);
|
|
510
578
|
console.warn = (...args) => {
|
|
511
579
|
this.originalConsoleWarn(...args);
|
|
512
|
-
const msg = args.map(
|
|
580
|
+
const msg = args.map(
|
|
581
|
+
(a) => typeof a === "string" ? a : a instanceof Error ? `${a.message}` : JSON.stringify(a)
|
|
582
|
+
).join(" ");
|
|
513
583
|
this.debugLog("warn", `[console.warn] ${msg.slice(0, 300)}`);
|
|
514
584
|
};
|
|
515
585
|
window.addEventListener("error", (e) => {
|
|
@@ -562,7 +632,7 @@ var _SiteManager = class _SiteManager {
|
|
|
562
632
|
this.debugOverlayEl = el;
|
|
563
633
|
this.setupGlobalErrorCapture();
|
|
564
634
|
this.debugLog("info", `[site-manager] debug overlay initialized`);
|
|
565
|
-
this.debugLog("info", `[site-manager] v1.1.
|
|
635
|
+
this.debugLog("info", `[site-manager] v1.1.19 | project: ${this.config.projectId}`);
|
|
566
636
|
this.renderDebugOverlay();
|
|
567
637
|
this.debugRefreshId = setInterval(() => this.renderDebugOverlay(), 1500);
|
|
568
638
|
}
|
|
@@ -744,7 +814,10 @@ var _SiteManager = class _SiteManager {
|
|
|
744
814
|
};
|
|
745
815
|
switch (action) {
|
|
746
816
|
case "test-event":
|
|
747
|
-
this.trackEvent("debug_test_event", {
|
|
817
|
+
this.trackEvent("debug_test_event", {
|
|
818
|
+
source: "debug_panel",
|
|
819
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
820
|
+
});
|
|
748
821
|
feedback("debug_test_event queued", true);
|
|
749
822
|
break;
|
|
750
823
|
case "flush": {
|
|
@@ -786,7 +859,10 @@ var _SiteManager = class _SiteManager {
|
|
|
786
859
|
case "copy-logs": {
|
|
787
860
|
const text = this.debugLogs.slice().reverse().map((l) => `[${l.ts}] [${l.level.toUpperCase()}] ${l.msg}`).join("\n");
|
|
788
861
|
void navigator.clipboard.writeText(text).then(() => {
|
|
789
|
-
feedback(
|
|
862
|
+
feedback(
|
|
863
|
+
`Copied ${this.debugLogs.length} log line${this.debugLogs.length !== 1 ? "s" : ""}`,
|
|
864
|
+
true
|
|
865
|
+
);
|
|
790
866
|
}).catch(() => {
|
|
791
867
|
feedback("Copy failed \u2014 clipboard not available", false);
|
|
792
868
|
});
|
|
@@ -904,9 +980,7 @@ var _SiteManager = class _SiteManager {
|
|
|
904
980
|
async checkVersion(currentVersion) {
|
|
905
981
|
const versionToCheck = currentVersion || this.config.appVersion;
|
|
906
982
|
if (!versionToCheck) {
|
|
907
|
-
console.warn(
|
|
908
|
-
"Featurely Site Manager: appVersion not provided for version check"
|
|
909
|
-
);
|
|
983
|
+
console.warn("Featurely Site Manager: appVersion not provided for version check");
|
|
910
984
|
return null;
|
|
911
985
|
}
|
|
912
986
|
try {
|
|
@@ -1050,7 +1124,10 @@ var _SiteManager = class _SiteManager {
|
|
|
1050
1124
|
this.debugLog("info", "[maintenance] ENABLED \u2014 user bypassed (whitelist match)");
|
|
1051
1125
|
return;
|
|
1052
1126
|
}
|
|
1053
|
-
this.debugLog(
|
|
1127
|
+
this.debugLog(
|
|
1128
|
+
"warn",
|
|
1129
|
+
`[maintenance] ENABLED (type: ${this.siteConfig.maintenance.type})`
|
|
1130
|
+
);
|
|
1054
1131
|
this.showMaintenancePage();
|
|
1055
1132
|
this.trackEvent("maintenance_enabled", {
|
|
1056
1133
|
maintenanceType: this.siteConfig.maintenance.type
|
|
@@ -1122,7 +1199,17 @@ var _SiteManager = class _SiteManager {
|
|
|
1122
1199
|
"button"
|
|
1123
1200
|
],
|
|
1124
1201
|
ALLOWED_ATTR: ["class", "id", "href", "src", "alt", "title", "style"],
|
|
1125
|
-
FORBID_TAGS: [
|
|
1202
|
+
FORBID_TAGS: [
|
|
1203
|
+
"script",
|
|
1204
|
+
"iframe",
|
|
1205
|
+
"embed",
|
|
1206
|
+
"object",
|
|
1207
|
+
"applet",
|
|
1208
|
+
"meta",
|
|
1209
|
+
"link",
|
|
1210
|
+
"form",
|
|
1211
|
+
"input"
|
|
1212
|
+
],
|
|
1126
1213
|
FORBID_ATTR: ["onerror", "onload", "onclick", "onmouseover"],
|
|
1127
1214
|
ALLOW_DATA_ATTR: false
|
|
1128
1215
|
});
|
|
@@ -1190,10 +1277,16 @@ var _SiteManager = class _SiteManager {
|
|
|
1190
1277
|
this.messageContainers.delete(id);
|
|
1191
1278
|
}
|
|
1192
1279
|
});
|
|
1193
|
-
this.debugLog(
|
|
1280
|
+
this.debugLog(
|
|
1281
|
+
"info",
|
|
1282
|
+
`[messages] ${activeMessages.length} active (${(_b = (_a = this.siteConfig) == null ? void 0 : _a.messages.length) != null ? _b : 0} total)`
|
|
1283
|
+
);
|
|
1194
1284
|
activeMessages.forEach((message) => {
|
|
1195
1285
|
if (!this.messageContainers.has(message.id)) {
|
|
1196
|
-
this.debugLog(
|
|
1286
|
+
this.debugLog(
|
|
1287
|
+
"info",
|
|
1288
|
+
`[messages] showing "${message.title}" (${message.type}, ${message.style})`
|
|
1289
|
+
);
|
|
1197
1290
|
this.showMessage(message);
|
|
1198
1291
|
if (this.config.onMessageReceived) {
|
|
1199
1292
|
this.config.onMessageReceived(message);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "featurely-site-manager",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.19",
|
|
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
|
-
"
|
|
58
|
+
"@neondatabase/serverless": "^1.0.2",
|
|
59
|
+
"dompurify": "^3.3.3",
|
|
60
|
+
"drizzle-orm": "^0.45.2"
|
|
58
61
|
}
|
|
59
62
|
}
|