featurely-site-manager 1.1.11 → 1.1.14
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 +23 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +424 -15
- package/dist/index.mjs +424 -15
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -90,6 +90,8 @@ interface SiteManagerConfig {
|
|
|
90
90
|
onUpdateAvailable?: (versionInfo: VersionCheckResponse) => void;
|
|
91
91
|
onUpdateRequired?: (versionInfo: VersionCheckResponse) => void;
|
|
92
92
|
onError?: (error: Error) => void;
|
|
93
|
+
debugMode?: boolean;
|
|
94
|
+
environment?: string;
|
|
93
95
|
}
|
|
94
96
|
declare class SiteManager {
|
|
95
97
|
private config;
|
|
@@ -109,6 +111,20 @@ declare class SiteManager {
|
|
|
109
111
|
private pageEntryTime;
|
|
110
112
|
private static readonly MAX_CONSECUTIVE_FAILURES;
|
|
111
113
|
private lastVersionCheck;
|
|
114
|
+
private debugOverlayEl;
|
|
115
|
+
private debugRefreshId;
|
|
116
|
+
private debugLogs;
|
|
117
|
+
private networkLog;
|
|
118
|
+
private analyticsEventsSent;
|
|
119
|
+
private debugMinimized;
|
|
120
|
+
private debugTab;
|
|
121
|
+
private recentAnalyticsEvents;
|
|
122
|
+
private testFlagKey;
|
|
123
|
+
private testFeedback;
|
|
124
|
+
private errorCount;
|
|
125
|
+
private consoleIntercepted;
|
|
126
|
+
private originalConsoleError;
|
|
127
|
+
private originalConsoleWarn;
|
|
112
128
|
constructor(config: SiteManagerConfig);
|
|
113
129
|
init(): Promise<void>;
|
|
114
130
|
destroy(): void;
|
|
@@ -127,6 +143,13 @@ declare class SiteManager {
|
|
|
127
143
|
private startAnalyticsFlushing;
|
|
128
144
|
private stopAnalyticsFlushing;
|
|
129
145
|
private flushAnalytics;
|
|
146
|
+
private debugLog;
|
|
147
|
+
private debugNetwork;
|
|
148
|
+
private stopDebugOverlay;
|
|
149
|
+
private setupGlobalErrorCapture;
|
|
150
|
+
private setupDebugOverlay;
|
|
151
|
+
private renderDebugOverlay;
|
|
152
|
+
private handleTestAction;
|
|
130
153
|
private setupPageTracking;
|
|
131
154
|
private trackPageView;
|
|
132
155
|
private trackPageExit;
|
package/dist/index.d.ts
CHANGED
|
@@ -90,6 +90,8 @@ interface SiteManagerConfig {
|
|
|
90
90
|
onUpdateAvailable?: (versionInfo: VersionCheckResponse) => void;
|
|
91
91
|
onUpdateRequired?: (versionInfo: VersionCheckResponse) => void;
|
|
92
92
|
onError?: (error: Error) => void;
|
|
93
|
+
debugMode?: boolean;
|
|
94
|
+
environment?: string;
|
|
93
95
|
}
|
|
94
96
|
declare class SiteManager {
|
|
95
97
|
private config;
|
|
@@ -109,6 +111,20 @@ declare class SiteManager {
|
|
|
109
111
|
private pageEntryTime;
|
|
110
112
|
private static readonly MAX_CONSECUTIVE_FAILURES;
|
|
111
113
|
private lastVersionCheck;
|
|
114
|
+
private debugOverlayEl;
|
|
115
|
+
private debugRefreshId;
|
|
116
|
+
private debugLogs;
|
|
117
|
+
private networkLog;
|
|
118
|
+
private analyticsEventsSent;
|
|
119
|
+
private debugMinimized;
|
|
120
|
+
private debugTab;
|
|
121
|
+
private recentAnalyticsEvents;
|
|
122
|
+
private testFlagKey;
|
|
123
|
+
private testFeedback;
|
|
124
|
+
private errorCount;
|
|
125
|
+
private consoleIntercepted;
|
|
126
|
+
private originalConsoleError;
|
|
127
|
+
private originalConsoleWarn;
|
|
112
128
|
constructor(config: SiteManagerConfig);
|
|
113
129
|
init(): Promise<void>;
|
|
114
130
|
destroy(): void;
|
|
@@ -127,6 +143,13 @@ declare class SiteManager {
|
|
|
127
143
|
private startAnalyticsFlushing;
|
|
128
144
|
private stopAnalyticsFlushing;
|
|
129
145
|
private flushAnalytics;
|
|
146
|
+
private debugLog;
|
|
147
|
+
private debugNetwork;
|
|
148
|
+
private stopDebugOverlay;
|
|
149
|
+
private setupGlobalErrorCapture;
|
|
150
|
+
private setupDebugOverlay;
|
|
151
|
+
private renderDebugOverlay;
|
|
152
|
+
private handleTestAction;
|
|
130
153
|
private setupPageTracking;
|
|
131
154
|
private trackPageView;
|
|
132
155
|
private trackPageExit;
|
package/dist/index.js
CHANGED
|
@@ -53,7 +53,23 @@ var _SiteManager = class _SiteManager {
|
|
|
53
53
|
this.currentPageSearch = "";
|
|
54
54
|
this.pageEntryTime = 0;
|
|
55
55
|
this.lastVersionCheck = null;
|
|
56
|
-
|
|
56
|
+
this.debugOverlayEl = null;
|
|
57
|
+
this.debugRefreshId = null;
|
|
58
|
+
this.debugLogs = [];
|
|
59
|
+
this.networkLog = [];
|
|
60
|
+
this.analyticsEventsSent = 0;
|
|
61
|
+
this.debugMinimized = false;
|
|
62
|
+
this.debugTab = "sdk";
|
|
63
|
+
this.recentAnalyticsEvents = [];
|
|
64
|
+
this.testFlagKey = "";
|
|
65
|
+
this.testFeedback = [];
|
|
66
|
+
this.errorCount = 0;
|
|
67
|
+
this.consoleIntercepted = false;
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
69
|
+
this.originalConsoleError = null;
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
71
|
+
this.originalConsoleWarn = null;
|
|
72
|
+
var _a, _b, _c, _d, _e, _f;
|
|
57
73
|
if (!config.apiKey) {
|
|
58
74
|
throw new Error("Featurely Site Manager: apiKey is required");
|
|
59
75
|
}
|
|
@@ -81,7 +97,9 @@ var _SiteManager = class _SiteManager {
|
|
|
81
97
|
versionCheckInterval: (_e = config.versionCheckInterval) != null ? _e : 36e5,
|
|
82
98
|
// 1 hour
|
|
83
99
|
onUpdateAvailable: config.onUpdateAvailable,
|
|
84
|
-
onUpdateRequired: config.onUpdateRequired
|
|
100
|
+
onUpdateRequired: config.onUpdateRequired,
|
|
101
|
+
debugMode: (_f = config.debugMode) != null ? _f : false,
|
|
102
|
+
environment: config.environment
|
|
85
103
|
};
|
|
86
104
|
this.loadDismissedMessages();
|
|
87
105
|
}
|
|
@@ -89,23 +107,34 @@ var _SiteManager = class _SiteManager {
|
|
|
89
107
|
* Initialize and start the site manager
|
|
90
108
|
*/
|
|
91
109
|
async init() {
|
|
110
|
+
var _a;
|
|
92
111
|
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
93
112
|
console.warn(
|
|
94
113
|
"Featurely Site Manager: Can only be initialized in a browser environment"
|
|
95
114
|
);
|
|
96
115
|
return;
|
|
97
116
|
}
|
|
98
|
-
|
|
99
|
-
|
|
117
|
+
if (this.config.debugMode) {
|
|
118
|
+
this.setupDebugOverlay();
|
|
119
|
+
}
|
|
120
|
+
this.debugLog("info", `[init] v1.1.14 | project: ${this.config.projectId} | env: ${(_a = this.config.environment) != null ? _a : "\u2014"}`);
|
|
121
|
+
this.debugLog("info", `[init] analytics: ${this.config.enableAnalytics ? "on" : "off"} | poll: ${this.config.pollInterval}ms | debug: ${this.config.debugMode ? "on" : "off"}`);
|
|
100
122
|
if (this.config.enableAnalytics) {
|
|
123
|
+
this.debugLog("info", "[init] starting analytics + page tracking");
|
|
101
124
|
this.startAnalyticsFlushing();
|
|
102
125
|
this.setupPageTracking();
|
|
103
126
|
}
|
|
127
|
+
this.debugLog("info", "[init] fetching initial config\u2026");
|
|
128
|
+
await this.fetchConfig();
|
|
129
|
+
this.startPolling();
|
|
130
|
+
this.debugLog("info", `[init] polling every ${this.config.pollInterval / 1e3}s`);
|
|
104
131
|
if (this.config.enableVersionCheck && this.config.appVersion) {
|
|
132
|
+
this.debugLog("info", `[init] checking version (current: ${this.config.appVersion})`);
|
|
105
133
|
await this.checkVersion();
|
|
106
134
|
this.startVersionChecking();
|
|
107
135
|
}
|
|
108
136
|
this.injectStyles();
|
|
137
|
+
this.debugLog("info", "[init] \u2713 ready");
|
|
109
138
|
}
|
|
110
139
|
/**
|
|
111
140
|
* Stop the site manager and clean up
|
|
@@ -115,6 +144,7 @@ var _SiteManager = class _SiteManager {
|
|
|
115
144
|
this.stopVersionChecking();
|
|
116
145
|
this.stopAnalyticsFlushing();
|
|
117
146
|
this.flushAnalytics();
|
|
147
|
+
this.stopDebugOverlay();
|
|
118
148
|
this.clearMessages();
|
|
119
149
|
}
|
|
120
150
|
/**
|
|
@@ -150,6 +180,7 @@ var _SiteManager = class _SiteManager {
|
|
|
150
180
|
return false;
|
|
151
181
|
}
|
|
152
182
|
const isEnabled = this.evaluateFeatureFlag(flag);
|
|
183
|
+
this.debugLog("info", `[flag] "${flagKey}" \u2192 ${isEnabled ? "ENABLED" : "DISABLED"}`);
|
|
153
184
|
const trackKey = `feature_flag_tracked_${flagKey}`;
|
|
154
185
|
if (typeof sessionStorage !== "undefined" && !sessionStorage.getItem(trackKey)) {
|
|
155
186
|
this.trackEvent("feature_flag_evaluated", {
|
|
@@ -273,6 +304,9 @@ var _SiteManager = class _SiteManager {
|
|
|
273
304
|
properties,
|
|
274
305
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
275
306
|
});
|
|
307
|
+
this.recentAnalyticsEvents.unshift({ name: eventName, ts: (/* @__PURE__ */ new Date()).toLocaleTimeString() });
|
|
308
|
+
if (this.recentAnalyticsEvents.length > 50) this.recentAnalyticsEvents.length = 50;
|
|
309
|
+
this.debugLog("info", `trackEvent: ${eventName}`);
|
|
276
310
|
if (this.analyticsQueue.length >= 10) {
|
|
277
311
|
this.flushAnalytics();
|
|
278
312
|
}
|
|
@@ -281,8 +315,9 @@ var _SiteManager = class _SiteManager {
|
|
|
281
315
|
// Configuration Fetching
|
|
282
316
|
// ============================================================================
|
|
283
317
|
async fetchConfig() {
|
|
284
|
-
var _a, _b;
|
|
318
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
285
319
|
try {
|
|
320
|
+
const fetchStartMs = Date.now();
|
|
286
321
|
const response = await fetch(
|
|
287
322
|
`${this.config.apiUrl}/api/public/v1/site-config?projectId=${this.config.projectId}`,
|
|
288
323
|
{
|
|
@@ -291,6 +326,7 @@ var _SiteManager = class _SiteManager {
|
|
|
291
326
|
}
|
|
292
327
|
}
|
|
293
328
|
);
|
|
329
|
+
this.debugNetwork(`/api/public/v1/site-config`, response.status, Date.now() - fetchStartMs);
|
|
294
330
|
if (!response.ok) {
|
|
295
331
|
const errorData = await response.json().catch(() => ({}));
|
|
296
332
|
const message = errorData.error || response.statusText;
|
|
@@ -313,9 +349,30 @@ var _SiteManager = class _SiteManager {
|
|
|
313
349
|
const configChanged = JSON.stringify(newConfig) !== JSON.stringify(this.siteConfig);
|
|
314
350
|
this.consecutiveFetchFailures = 0;
|
|
315
351
|
if (configChanged) {
|
|
316
|
-
const
|
|
317
|
-
const
|
|
352
|
+
const prevMaintenance = (_b = (_a = this.siteConfig) == null ? void 0 : _a.maintenance.enabled) != null ? _b : false;
|
|
353
|
+
const prevFlagCount = (_d = (_c = this.siteConfig) == null ? void 0 : _c.featureFlags.length) != null ? _d : 0;
|
|
354
|
+
const prevMsgCount = (_f = (_e = this.siteConfig) == null ? void 0 : _e.messages.length) != null ? _f : 0;
|
|
355
|
+
const delta = [];
|
|
356
|
+
if (newConfig.maintenance.enabled !== prevMaintenance)
|
|
357
|
+
delta.push(`maintenance ${newConfig.maintenance.enabled ? "ON" : "OFF"}`);
|
|
358
|
+
if (newConfig.featureFlags.length !== prevFlagCount)
|
|
359
|
+
delta.push(`flags: ${prevFlagCount}\u2192${newConfig.featureFlags.length}`);
|
|
360
|
+
if (newConfig.messages.length !== prevMsgCount)
|
|
361
|
+
delta.push(`messages: ${prevMsgCount}\u2192${newConfig.messages.length}`);
|
|
362
|
+
this.debugLog("info", `[config] updated${delta.length ? ": " + delta.join(", ") : " (structure changed)"}`);
|
|
363
|
+
const wasMaintenanceEnabled = (_g = this.siteConfig) == null ? void 0 : _g.maintenance.enabled;
|
|
364
|
+
const oldFeatureFlags = ((_h = this.siteConfig) == null ? void 0 : _h.featureFlags) || [];
|
|
318
365
|
this.siteConfig = newConfig;
|
|
366
|
+
if (newConfig.debugMode === true && !this.debugOverlayEl) {
|
|
367
|
+
this.setupDebugOverlay();
|
|
368
|
+
}
|
|
369
|
+
if (!this.debugOverlayEl && this.config.environment) {
|
|
370
|
+
const envs = newConfig.environments;
|
|
371
|
+
const env = envs == null ? void 0 : envs.find((e) => e.slug === this.config.environment || e.name === this.config.environment);
|
|
372
|
+
if ((env == null ? void 0 : env.debugEnabled) === true) {
|
|
373
|
+
this.setupDebugOverlay();
|
|
374
|
+
}
|
|
375
|
+
}
|
|
319
376
|
if (newConfig.maintenance.enabled && !wasMaintenanceEnabled) {
|
|
320
377
|
this.enableMaintenanceMode();
|
|
321
378
|
} else if (!newConfig.maintenance.enabled && wasMaintenanceEnabled) {
|
|
@@ -329,17 +386,22 @@ var _SiteManager = class _SiteManager {
|
|
|
329
386
|
}
|
|
330
387
|
}
|
|
331
388
|
this.updateMessages();
|
|
389
|
+
} else {
|
|
390
|
+
this.debugLog("info", "[config] polled \u2014 no changes");
|
|
332
391
|
}
|
|
333
392
|
} catch (error) {
|
|
334
393
|
this.consecutiveFetchFailures++;
|
|
394
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
395
|
+
this.debugLog("error", `[config] fetch failed (attempt ${this.consecutiveFetchFailures}): ${errMsg}`);
|
|
396
|
+
this.errorCount++;
|
|
335
397
|
if (this.consecutiveFetchFailures <= _SiteManager.MAX_CONSECUTIVE_FAILURES) {
|
|
336
398
|
const isNetworkError = error instanceof TypeError && (error.message.includes("NetworkError") || error.message.includes("Failed to fetch") || error.message.includes("fetch"));
|
|
337
399
|
if (isNetworkError) {
|
|
338
|
-
|
|
339
|
-
`[Featurely] Network error \u2014 request to Featurely was blocked.
|
|
400
|
+
const cspMsg = `[Featurely] Network error \u2014 request to Featurely was blocked.
|
|
340
401
|
\u2192 If your site has a Content-Security-Policy, add 'https://www.featurely.no' to the connect-src directive.
|
|
341
|
-
Example: connect-src 'self' https://www.featurely.no
|
|
342
|
-
);
|
|
402
|
+
Example: connect-src 'self' https://www.featurely.no ...`;
|
|
403
|
+
console.error(cspMsg);
|
|
404
|
+
this.debugLog("error", "[config] possible CSP block \u2014 check connect-src");
|
|
343
405
|
} else {
|
|
344
406
|
console.error("Featurely Site Manager: Failed to fetch configuration", error);
|
|
345
407
|
}
|
|
@@ -347,9 +409,9 @@ var _SiteManager = class _SiteManager {
|
|
|
347
409
|
this.config.onError(error);
|
|
348
410
|
}
|
|
349
411
|
} else if (this.consecutiveFetchFailures === _SiteManager.MAX_CONSECUTIVE_FAILURES + 1) {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
);
|
|
412
|
+
const silenceMsg = `Featurely Site Manager: Silencing repeated fetch errors after ${_SiteManager.MAX_CONSECUTIVE_FAILURES} failures. Check your apiUrl and Content-Security-Policy configuration.`;
|
|
413
|
+
console.error(silenceMsg);
|
|
414
|
+
this.debugLog("error", `[config] silencing errors after ${_SiteManager.MAX_CONSECUTIVE_FAILURES} consecutive failures`);
|
|
353
415
|
}
|
|
354
416
|
}
|
|
355
417
|
}
|
|
@@ -387,6 +449,8 @@ var _SiteManager = class _SiteManager {
|
|
|
387
449
|
const url = `${this.config.apiUrl}/api/projects/${this.config.projectId}/analytics/events`;
|
|
388
450
|
const eventsToSend = [...this.analyticsQueue];
|
|
389
451
|
this.analyticsQueue = [];
|
|
452
|
+
this.analyticsEventsSent += eventsToSend.length;
|
|
453
|
+
this.debugLog("info", `[analytics] flushing ${eventsToSend.length} event(s) (total sent: ${this.analyticsEventsSent})`);
|
|
390
454
|
for (const event of eventsToSend) {
|
|
391
455
|
const payload = JSON.stringify({
|
|
392
456
|
eventName: event.eventName,
|
|
@@ -403,25 +467,364 @@ var _SiteManager = class _SiteManager {
|
|
|
403
467
|
}
|
|
404
468
|
} else {
|
|
405
469
|
try {
|
|
470
|
+
const t0 = Date.now();
|
|
406
471
|
const res = await fetch(url, {
|
|
407
472
|
method: "POST",
|
|
408
473
|
headers: { "Content-Type": "application/json" },
|
|
409
474
|
body: payload,
|
|
410
475
|
keepalive: true
|
|
411
476
|
});
|
|
477
|
+
this.debugNetwork(`/api/projects/${this.config.projectId}/analytics/events`, res.status, Date.now() - t0);
|
|
412
478
|
if (!res.ok) {
|
|
413
479
|
const body = await res.text().catch(() => "(unreadable)");
|
|
414
|
-
|
|
480
|
+
const errDetail = `[analytics] event "${event.eventName}" rejected: ${res.status} ${res.statusText} \u2014 ${body}`;
|
|
481
|
+
console.error(`[Featurely] ${errDetail}`);
|
|
482
|
+
this.debugLog("error", errDetail);
|
|
483
|
+
this.errorCount++;
|
|
415
484
|
}
|
|
416
485
|
} catch (fetchError) {
|
|
417
486
|
const sentViaBeacon = typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && navigator.sendBeacon(url, payload);
|
|
418
487
|
if (!sentViaBeacon) {
|
|
488
|
+
const errMsg = fetchError instanceof Error ? fetchError.message : String(fetchError);
|
|
419
489
|
console.error(`[Featurely] Failed to send analytics event "${event.eventName}":`, fetchError);
|
|
490
|
+
this.debugLog("error", `[analytics] send failed for "${event.eventName}": ${errMsg}`);
|
|
491
|
+
this.errorCount++;
|
|
492
|
+
} else {
|
|
493
|
+
this.debugLog("warn", `[analytics] fetch failed for "${event.eventName}", delivered via sendBeacon`);
|
|
420
494
|
}
|
|
421
495
|
}
|
|
422
496
|
}
|
|
423
497
|
}
|
|
424
498
|
}
|
|
499
|
+
/** Log a message to the debug panel (max 100 entries). */
|
|
500
|
+
debugLog(level, msg) {
|
|
501
|
+
if (!this.config.debugMode && !this.debugOverlayEl) return;
|
|
502
|
+
const ts = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
503
|
+
this.debugLogs.unshift({ ts, level, msg });
|
|
504
|
+
if (this.debugLogs.length > 100) this.debugLogs.length = 100;
|
|
505
|
+
}
|
|
506
|
+
/** Add a network call entry. */
|
|
507
|
+
debugNetwork(url, status, ms) {
|
|
508
|
+
if (!this.config.debugMode && !this.debugOverlayEl) return;
|
|
509
|
+
const ts = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
510
|
+
const shortUrl = url.replace(/^https?:\/\/[^/]+/, "");
|
|
511
|
+
this.networkLog.unshift({ url: shortUrl, status, ms, ts });
|
|
512
|
+
if (this.networkLog.length > 50) this.networkLog.length = 50;
|
|
513
|
+
}
|
|
514
|
+
stopDebugOverlay() {
|
|
515
|
+
var _a;
|
|
516
|
+
if (this.debugRefreshId) {
|
|
517
|
+
clearInterval(this.debugRefreshId);
|
|
518
|
+
this.debugRefreshId = null;
|
|
519
|
+
}
|
|
520
|
+
if ((_a = this.debugOverlayEl) == null ? void 0 : _a.parentNode) {
|
|
521
|
+
this.debugOverlayEl.parentNode.removeChild(this.debugOverlayEl);
|
|
522
|
+
}
|
|
523
|
+
this.debugOverlayEl = null;
|
|
524
|
+
if (this.originalConsoleError) {
|
|
525
|
+
console.error = this.originalConsoleError;
|
|
526
|
+
this.originalConsoleError = null;
|
|
527
|
+
}
|
|
528
|
+
if (this.originalConsoleWarn) {
|
|
529
|
+
console.warn = this.originalConsoleWarn;
|
|
530
|
+
this.originalConsoleWarn = null;
|
|
531
|
+
}
|
|
532
|
+
this.consoleIntercepted = false;
|
|
533
|
+
}
|
|
534
|
+
setupGlobalErrorCapture() {
|
|
535
|
+
if (this.consoleIntercepted) return;
|
|
536
|
+
this.consoleIntercepted = true;
|
|
537
|
+
this.originalConsoleError = console.error.bind(console);
|
|
538
|
+
console.error = (...args) => {
|
|
539
|
+
this.originalConsoleError(...args);
|
|
540
|
+
const msg = args.map((a) => typeof a === "string" ? a : a instanceof Error ? `${a.message}` : JSON.stringify(a)).join(" ");
|
|
541
|
+
this.errorCount++;
|
|
542
|
+
this.debugLog("error", `[console.error] ${msg.slice(0, 300)}`);
|
|
543
|
+
};
|
|
544
|
+
this.originalConsoleWarn = console.warn.bind(console);
|
|
545
|
+
console.warn = (...args) => {
|
|
546
|
+
this.originalConsoleWarn(...args);
|
|
547
|
+
const msg = args.map((a) => typeof a === "string" ? a : a instanceof Error ? `${a.message}` : JSON.stringify(a)).join(" ");
|
|
548
|
+
this.debugLog("warn", `[console.warn] ${msg.slice(0, 300)}`);
|
|
549
|
+
};
|
|
550
|
+
window.addEventListener("error", (e) => {
|
|
551
|
+
this.errorCount++;
|
|
552
|
+
const src = e.filename ? `${e.filename.replace(/.*\//, "")}:${e.lineno}:${e.colno}` : "";
|
|
553
|
+
this.debugLog("error", `[uncaught error] ${e.message}${src ? ` @ ${src}` : ""}`);
|
|
554
|
+
});
|
|
555
|
+
window.addEventListener("unhandledrejection", (e) => {
|
|
556
|
+
this.errorCount++;
|
|
557
|
+
const reason = e.reason instanceof Error ? e.reason.message : typeof e.reason === "string" ? e.reason : JSON.stringify(e.reason);
|
|
558
|
+
this.debugLog("error", `[unhandled promise] ${reason.slice(0, 300)}`);
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
setupDebugOverlay() {
|
|
562
|
+
if (typeof document === "undefined" || this.debugOverlayEl) return;
|
|
563
|
+
const el = document.createElement("div");
|
|
564
|
+
el.id = "__featurely_debug__";
|
|
565
|
+
el.setAttribute("data-featurely-debug", "true");
|
|
566
|
+
const BG = "rgba(15,15,20,0.97)";
|
|
567
|
+
const BORDER = "rgba(255,255,255,0.12)";
|
|
568
|
+
const TEXT = "#e5e7eb";
|
|
569
|
+
const W = "360px";
|
|
570
|
+
Object.assign(el.style, {
|
|
571
|
+
position: "fixed",
|
|
572
|
+
bottom: "16px",
|
|
573
|
+
right: "16px",
|
|
574
|
+
width: W,
|
|
575
|
+
zIndex: "2147483647",
|
|
576
|
+
fontFamily: "'SF Mono', 'Fira Code', 'Cascadia Code', monospace",
|
|
577
|
+
fontSize: "11px",
|
|
578
|
+
lineHeight: "1.5",
|
|
579
|
+
color: TEXT,
|
|
580
|
+
background: BG,
|
|
581
|
+
border: `1px solid ${BORDER}`,
|
|
582
|
+
borderRadius: "10px",
|
|
583
|
+
boxShadow: "0 8px 32px rgba(0,0,0,0.5)",
|
|
584
|
+
overflow: "hidden",
|
|
585
|
+
userSelect: "none",
|
|
586
|
+
transition: "height 0.15s ease"
|
|
587
|
+
});
|
|
588
|
+
window.__FEATURELY_DEBUG__ = {
|
|
589
|
+
log: (pkg, level, msg) => {
|
|
590
|
+
this.debugLog(level, `[${pkg}] ${msg}`);
|
|
591
|
+
},
|
|
592
|
+
registerPackage: (pkg, getState) => {
|
|
593
|
+
this[`__debug_${pkg}`] = getState;
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
document.body.appendChild(el);
|
|
597
|
+
this.debugOverlayEl = el;
|
|
598
|
+
this.setupGlobalErrorCapture();
|
|
599
|
+
this.debugLog("info", `[site-manager] debug overlay initialized`);
|
|
600
|
+
this.debugLog("info", `[site-manager] v1.1.14 | project: ${this.config.projectId}`);
|
|
601
|
+
this.renderDebugOverlay();
|
|
602
|
+
this.debugRefreshId = setInterval(() => this.renderDebugOverlay(), 1500);
|
|
603
|
+
}
|
|
604
|
+
renderDebugOverlay() {
|
|
605
|
+
var _a, _b, _c, _d, _e;
|
|
606
|
+
const el = this.debugOverlayEl;
|
|
607
|
+
if (!el) return;
|
|
608
|
+
const ACCENT = "#6366f1";
|
|
609
|
+
const BORDER = "rgba(255,255,255,0.12)";
|
|
610
|
+
const TEXT = "#e5e7eb";
|
|
611
|
+
const MUTED = "#9ca3af";
|
|
612
|
+
const GREEN = "#22c55e";
|
|
613
|
+
const RED = "#ef4444";
|
|
614
|
+
const YELLOW = "#eab308";
|
|
615
|
+
const tab = this.debugTab;
|
|
616
|
+
const minimized = this.debugMinimized;
|
|
617
|
+
const cfg = this.config;
|
|
618
|
+
const sc = this.siteConfig;
|
|
619
|
+
const tabBtn = (id, label, badgeCount) => {
|
|
620
|
+
const badgeHtml = badgeCount && badgeCount > 0 ? `<span style="display:inline-flex;align-items:center;justify-content:center;min-width:14px;height:14px;padding:0 3px;margin-left:3px;border-radius:9999px;font-size:8px;font-weight:700;background:${RED};color:#fff;vertical-align:middle">${badgeCount > 9 ? "9+" : badgeCount}</span>` : "";
|
|
621
|
+
return `<button data-tab="${id}" style="padding:3px 10px;border-radius:4px;border:none;cursor:pointer;font-size:10px;font-family:inherit;background:${tab === id ? ACCENT : "rgba(255,255,255,0.06)"};color:${tab === id ? "#fff" : MUTED};transition:background 0.1s">${label}${badgeHtml}</button>`;
|
|
622
|
+
};
|
|
623
|
+
const badge = (text, color) => `<span style="padding:1px 6px;border-radius:9999px;font-size:9px;font-weight:600;background:${color}22;color:${color}">${text}</span>`;
|
|
624
|
+
const row = (label, value) => `<div style="display:flex;justify-content:space-between;align-items:center;padding:3px 0;border-bottom:1px solid rgba(255,255,255,0.04)"><span style="color:${MUTED}">${label}</span><span style="color:${TEXT};word-break:break-all;text-align:right;max-width:220px">${value}</span></div>`;
|
|
625
|
+
let content = "";
|
|
626
|
+
if (!minimized) {
|
|
627
|
+
if (tab === "sdk") {
|
|
628
|
+
const maintenance = ((_a = sc == null ? void 0 : sc.maintenance) == null ? void 0 : _a.enabled) ? badge("ON", RED) : badge("OFF", GREEN);
|
|
629
|
+
const flagCount = (_c = (_b = sc == null ? void 0 : sc.featureFlags) == null ? void 0 : _b.length) != null ? _c : 0;
|
|
630
|
+
const msgCount = (_e = (_d = sc == null ? void 0 : sc.messages) == null ? void 0 : _d.length) != null ? _e : 0;
|
|
631
|
+
content = `
|
|
632
|
+
${row("SDK Version", "1.1.14")}
|
|
633
|
+
${row("Project", cfg.projectId)}
|
|
634
|
+
${row("Environment", cfg.environment || "\u2014")}
|
|
635
|
+
${row("API URL", cfg.apiUrl)}
|
|
636
|
+
${row("Maintenance", maintenance)}
|
|
637
|
+
${row("Feature Flags", `${flagCount} active`)}
|
|
638
|
+
${row("Status Messages", `${msgCount} active`)}
|
|
639
|
+
${row("Analytics Queue", `${this.analyticsQueue.length} pending`)}
|
|
640
|
+
${row("Events Sent", `${this.analyticsEventsSent}`)}
|
|
641
|
+
${row("Session ID", this.sessionId.slice(0, 16) + "\u2026")}
|
|
642
|
+
${row("User ID", cfg.userId || "\u2014")}
|
|
643
|
+
${row("Page", this.currentPagePath || "\u2014")}
|
|
644
|
+
`;
|
|
645
|
+
const pkgKeys = Object.keys(this).filter((k) => k.startsWith("__debug_"));
|
|
646
|
+
for (const k of pkgKeys) {
|
|
647
|
+
const fn = this[k];
|
|
648
|
+
if (typeof fn === "function") {
|
|
649
|
+
try {
|
|
650
|
+
content += `<div style="margin-top:6px;padding-top:4px;border-top:1px solid ${BORDER}">${fn()}</div>`;
|
|
651
|
+
} catch {
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
} else if (tab === "logs") {
|
|
656
|
+
const logs = this.debugLogs.slice(0, 50);
|
|
657
|
+
const errorsInLog = logs.filter((l) => l.level === "error").length;
|
|
658
|
+
const clearBtn = `<button data-action="clear-logs" style="background:rgba(255,255,255,0.08);border:none;color:${MUTED};cursor:pointer;font-size:9px;font-family:inherit;padding:2px 7px;border-radius:3px">Clear</button>`;
|
|
659
|
+
const errBadge = errorsInLog > 0 ? `<span style="font-size:9px;color:${RED}">${errorsInLog} error${errorsInLog > 1 ? "s" : ""}</span>` : "";
|
|
660
|
+
const logHeader = `<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">${errBadge || `<span></span>`}${clearBtn}</div>`;
|
|
661
|
+
if (logs.length === 0) {
|
|
662
|
+
content = logHeader + `<div style="color:${MUTED};padding:12px;text-align:center">No logs yet</div>`;
|
|
663
|
+
} else {
|
|
664
|
+
content = logHeader + logs.map((l) => {
|
|
665
|
+
const c = l.level === "error" ? RED : l.level === "warn" ? YELLOW : "#a5b4fc";
|
|
666
|
+
return `<div style="padding:2px 0;display:flex;gap:6px;align-items:flex-start;border-bottom:1px solid rgba(255,255,255,0.03)"><span style="color:${MUTED};white-space:nowrap;font-size:9px">${l.ts}</span><span style="color:${c};flex:1;word-break:break-word">${l.msg}</span></div>`;
|
|
667
|
+
}).join("");
|
|
668
|
+
}
|
|
669
|
+
} else if (tab === "network") {
|
|
670
|
+
const calls = this.networkLog.slice(0, 20);
|
|
671
|
+
if (calls.length === 0) {
|
|
672
|
+
content = `<div style="color:${MUTED};padding:16px;text-align:center">No network calls yet</div>`;
|
|
673
|
+
} else {
|
|
674
|
+
content = calls.map((n) => {
|
|
675
|
+
const ok = typeof n.status === "number" && n.status >= 200 && n.status < 300;
|
|
676
|
+
const sc2 = ok ? GREEN : RED;
|
|
677
|
+
return `<div style="padding:3px 0;border-bottom:1px solid rgba(255,255,255,0.03)"><div style="display:flex;justify-content:space-between"><span style="color:${sc2};font-weight:600">${n.status}</span><span style="color:${MUTED}">${n.ms}ms</span><span style="color:${MUTED}">${n.ts}</span></div><div style="color:${TEXT};word-break:break-all">${n.url}</div></div>`;
|
|
678
|
+
}).join("");
|
|
679
|
+
}
|
|
680
|
+
} else if (tab === "events") {
|
|
681
|
+
const events = this.recentAnalyticsEvents.slice(0, 30);
|
|
682
|
+
if (events.length === 0) {
|
|
683
|
+
content = `<div style="color:${MUTED};padding:16px;text-align:center">No events yet</div>`;
|
|
684
|
+
} else {
|
|
685
|
+
content = events.map(
|
|
686
|
+
(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>`
|
|
687
|
+
).join("");
|
|
688
|
+
}
|
|
689
|
+
} else if (tab === "test") {
|
|
690
|
+
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`;
|
|
691
|
+
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>`;
|
|
692
|
+
const feedbackHtml = this.testFeedback.slice(0, 6).map(
|
|
693
|
+
(f) => `<div style="display:flex;gap:6px;align-items:center;padding:2px 0;border-bottom:1px solid rgba(255,255,255,0.03)">
|
|
694
|
+
<span style="color:${f.ok ? GREEN : RED};font-size:10px">●</span>
|
|
695
|
+
<span style="color:${TEXT};flex:1">${f.msg}</span>
|
|
696
|
+
<span style="color:${MUTED}">${f.ts}</span>
|
|
697
|
+
</div>`
|
|
698
|
+
).join("");
|
|
699
|
+
content = `
|
|
700
|
+
${sectionLabel("Analytics")}
|
|
701
|
+
<div style="display:flex;gap:4px;flex-wrap:wrap">
|
|
702
|
+
<button data-action="test-event" style="${btnStyle(ACCENT)}">Send test event</button>
|
|
703
|
+
<button data-action="flush" style="${btnStyle("rgba(255,255,255,0.15)")}">Flush queue now</button>
|
|
704
|
+
</div>
|
|
705
|
+
${sectionLabel("Feature Flags")}
|
|
706
|
+
<div style="display:flex;gap:4px;align-items:center">
|
|
707
|
+
<input data-testinput="flagkey" value="${this.testFlagKey.replace(/"/g, """)}" placeholder="flag-key" style="flex:1;background:rgba(255,255,255,0.07);border:1px solid ${BORDER};border-radius:4px;color:${TEXT};font-family:inherit;font-size:10px;padding:3px 7px;outline:none" />
|
|
708
|
+
<button data-action="check-flag" style="${btnStyle(ACCENT)}">Check</button>
|
|
709
|
+
</div>
|
|
710
|
+
${sectionLabel("Errors & Bugs")}
|
|
711
|
+
<div style="display:flex;gap:4px;flex-wrap:wrap">
|
|
712
|
+
<button data-action="test-error" style="${btnStyle(RED)}">Send test error</button>
|
|
713
|
+
<button data-action="test-bug" style="${btnStyle(YELLOW)}">Send test bug</button>
|
|
714
|
+
</div>
|
|
715
|
+
${sectionLabel("Config")}
|
|
716
|
+
<div style="display:flex;gap:4px;flex-wrap:wrap">
|
|
717
|
+
<button data-action="refresh" style="${btnStyle("rgba(255,255,255,0.15)")}">Refresh config</button>
|
|
718
|
+
<button data-action="clear-logs" style="${btnStyle("rgba(255,255,255,0.15)")}">Clear logs</button>
|
|
719
|
+
</div>
|
|
720
|
+
${this.testFeedback.length > 0 ? `
|
|
721
|
+
<div style="margin-top:10px;padding-top:6px;border-top:1px solid ${BORDER}">${feedbackHtml}</div>
|
|
722
|
+
` : ""}
|
|
723
|
+
`;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
el.innerHTML = `
|
|
727
|
+
<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__">
|
|
728
|
+
<span style="color:${ACCENT};font-weight:700;font-size:10px;letter-spacing:0.05em">⬡ FEATURELY DEBUG</span>
|
|
729
|
+
<span style="flex:1"></span>
|
|
730
|
+
${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>`}
|
|
731
|
+
<button id="__ft_dbg_min__" style="background:none;border:none;color:${MUTED};cursor:pointer;font-size:14px;line-height:1;padding:0 2px" title="${minimized ? "Expand" : "Minimize"}">${minimized ? "⬆" : "⬇"}</button>
|
|
732
|
+
<button id="__ft_dbg_cls__" style="background:none;border:none;color:${MUTED};cursor:pointer;font-size:14px;line-height:1;padding:0 2px" title="Close">×</button>
|
|
733
|
+
</div>
|
|
734
|
+
${minimized ? "" : `<div style="padding:8px 10px;max-height:300px;overflow-y:auto">${content}</div>`}
|
|
735
|
+
`;
|
|
736
|
+
el.querySelectorAll("[data-tab]").forEach((btn) => {
|
|
737
|
+
btn.onclick = (e) => {
|
|
738
|
+
e.stopPropagation();
|
|
739
|
+
this.debugTab = btn.dataset.tab;
|
|
740
|
+
this.renderDebugOverlay();
|
|
741
|
+
};
|
|
742
|
+
});
|
|
743
|
+
const minBtn = el.querySelector("#__ft_dbg_min__");
|
|
744
|
+
if (minBtn) {
|
|
745
|
+
minBtn.onclick = (e) => {
|
|
746
|
+
e.stopPropagation();
|
|
747
|
+
this.debugMinimized = !this.debugMinimized;
|
|
748
|
+
this.renderDebugOverlay();
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
const clsBtn = el.querySelector("#__ft_dbg_cls__");
|
|
752
|
+
if (clsBtn) {
|
|
753
|
+
clsBtn.onclick = (e) => {
|
|
754
|
+
e.stopPropagation();
|
|
755
|
+
this.stopDebugOverlay();
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
el.querySelectorAll("[data-action]").forEach((btn) => {
|
|
759
|
+
btn.onclick = (e) => {
|
|
760
|
+
e.stopPropagation();
|
|
761
|
+
void this.handleTestAction(btn.dataset.action);
|
|
762
|
+
};
|
|
763
|
+
});
|
|
764
|
+
el.querySelectorAll("[data-testinput='flagkey']").forEach((input) => {
|
|
765
|
+
input.oninput = (e) => {
|
|
766
|
+
e.stopPropagation();
|
|
767
|
+
this.testFlagKey = e.target.value;
|
|
768
|
+
};
|
|
769
|
+
input.onclick = (e) => e.stopPropagation();
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
async handleTestAction(action) {
|
|
773
|
+
const ts = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
774
|
+
const feedback = (msg, ok) => {
|
|
775
|
+
this.testFeedback.unshift({ msg, ok, ts });
|
|
776
|
+
if (this.testFeedback.length > 10) this.testFeedback.length = 10;
|
|
777
|
+
this.renderDebugOverlay();
|
|
778
|
+
};
|
|
779
|
+
switch (action) {
|
|
780
|
+
case "test-event":
|
|
781
|
+
this.trackEvent("debug_test_event", { source: "debug_panel", ts: (/* @__PURE__ */ new Date()).toISOString() });
|
|
782
|
+
feedback("debug_test_event queued", true);
|
|
783
|
+
break;
|
|
784
|
+
case "flush": {
|
|
785
|
+
const before = this.analyticsQueue.length;
|
|
786
|
+
await this.flushAnalytics();
|
|
787
|
+
feedback(`Flushed ${before} event(s)`, true);
|
|
788
|
+
break;
|
|
789
|
+
}
|
|
790
|
+
case "check-flag": {
|
|
791
|
+
const key = this.testFlagKey.trim();
|
|
792
|
+
if (!key) {
|
|
793
|
+
feedback("Enter a flag key first", false);
|
|
794
|
+
break;
|
|
795
|
+
}
|
|
796
|
+
const enabled = this.isFeatureEnabled(key);
|
|
797
|
+
feedback(`"${key}" \u2192 ${enabled ? "ENABLED \u2713" : "DISABLED \u2717"}`, enabled);
|
|
798
|
+
break;
|
|
799
|
+
}
|
|
800
|
+
case "test-error":
|
|
801
|
+
this.trackEvent("error_reported", {
|
|
802
|
+
message: "Test error from debug panel",
|
|
803
|
+
source: "debug_panel",
|
|
804
|
+
severity: "error"
|
|
805
|
+
});
|
|
806
|
+
feedback("error_reported queued", true);
|
|
807
|
+
break;
|
|
808
|
+
case "test-bug":
|
|
809
|
+
this.trackEvent("bug_reported", {
|
|
810
|
+
title: "Test bug from debug panel",
|
|
811
|
+
source: "debug_panel",
|
|
812
|
+
severity: "medium"
|
|
813
|
+
});
|
|
814
|
+
feedback("bug_reported queued", true);
|
|
815
|
+
break;
|
|
816
|
+
case "refresh":
|
|
817
|
+
await this.fetchConfig();
|
|
818
|
+
feedback("Config refreshed", true);
|
|
819
|
+
break;
|
|
820
|
+
case "clear-logs":
|
|
821
|
+
this.debugLogs = [];
|
|
822
|
+
this.testFeedback = [];
|
|
823
|
+
this.errorCount = 0;
|
|
824
|
+
this.renderDebugOverlay();
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
425
828
|
setupPageTracking() {
|
|
426
829
|
if (this.pageTrackingSetup) return;
|
|
427
830
|
this.pageTrackingSetup = true;
|
|
@@ -669,8 +1072,10 @@ var _SiteManager = class _SiteManager {
|
|
|
669
1072
|
enableMaintenanceMode() {
|
|
670
1073
|
if (!this.siteConfig) return;
|
|
671
1074
|
if (this.shouldBypassMaintenance()) {
|
|
1075
|
+
this.debugLog("info", "[maintenance] ENABLED \u2014 user bypassed (whitelist match)");
|
|
672
1076
|
return;
|
|
673
1077
|
}
|
|
1078
|
+
this.debugLog("warn", `[maintenance] ENABLED (type: ${this.siteConfig.maintenance.type})`);
|
|
674
1079
|
this.showMaintenancePage();
|
|
675
1080
|
this.trackEvent("maintenance_enabled", {
|
|
676
1081
|
maintenanceType: this.siteConfig.maintenance.type
|
|
@@ -680,6 +1085,7 @@ var _SiteManager = class _SiteManager {
|
|
|
680
1085
|
}
|
|
681
1086
|
}
|
|
682
1087
|
disableMaintenanceMode() {
|
|
1088
|
+
this.debugLog("info", "[maintenance] DISABLED \u2014 site back online");
|
|
683
1089
|
this.hideMaintenancePage();
|
|
684
1090
|
this.trackEvent("maintenance_disabled");
|
|
685
1091
|
if (this.config.onMaintenanceDisabled) {
|
|
@@ -774,6 +1180,7 @@ var _SiteManager = class _SiteManager {
|
|
|
774
1180
|
// Status Messages
|
|
775
1181
|
// ============================================================================
|
|
776
1182
|
updateMessages() {
|
|
1183
|
+
var _a, _b;
|
|
777
1184
|
if (!this.siteConfig) return;
|
|
778
1185
|
const currentTime = /* @__PURE__ */ new Date();
|
|
779
1186
|
const currentPath = window.location.pathname;
|
|
@@ -808,8 +1215,10 @@ var _SiteManager = class _SiteManager {
|
|
|
808
1215
|
this.messageContainers.delete(id);
|
|
809
1216
|
}
|
|
810
1217
|
});
|
|
1218
|
+
this.debugLog("info", `[messages] ${activeMessages.length} active (${(_b = (_a = this.siteConfig) == null ? void 0 : _a.messages.length) != null ? _b : 0} total)`);
|
|
811
1219
|
activeMessages.forEach((message) => {
|
|
812
1220
|
if (!this.messageContainers.has(message.id)) {
|
|
1221
|
+
this.debugLog("info", `[messages] showing "${message.title}" (${message.type}, ${message.style})`);
|
|
813
1222
|
this.showMessage(message);
|
|
814
1223
|
if (this.config.onMessageReceived) {
|
|
815
1224
|
this.config.onMessageReceived(message);
|
package/dist/index.mjs
CHANGED
|
@@ -18,7 +18,23 @@ var _SiteManager = class _SiteManager {
|
|
|
18
18
|
this.currentPageSearch = "";
|
|
19
19
|
this.pageEntryTime = 0;
|
|
20
20
|
this.lastVersionCheck = null;
|
|
21
|
-
|
|
21
|
+
this.debugOverlayEl = null;
|
|
22
|
+
this.debugRefreshId = null;
|
|
23
|
+
this.debugLogs = [];
|
|
24
|
+
this.networkLog = [];
|
|
25
|
+
this.analyticsEventsSent = 0;
|
|
26
|
+
this.debugMinimized = false;
|
|
27
|
+
this.debugTab = "sdk";
|
|
28
|
+
this.recentAnalyticsEvents = [];
|
|
29
|
+
this.testFlagKey = "";
|
|
30
|
+
this.testFeedback = [];
|
|
31
|
+
this.errorCount = 0;
|
|
32
|
+
this.consoleIntercepted = false;
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
34
|
+
this.originalConsoleError = null;
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
36
|
+
this.originalConsoleWarn = null;
|
|
37
|
+
var _a, _b, _c, _d, _e, _f;
|
|
22
38
|
if (!config.apiKey) {
|
|
23
39
|
throw new Error("Featurely Site Manager: apiKey is required");
|
|
24
40
|
}
|
|
@@ -46,7 +62,9 @@ var _SiteManager = class _SiteManager {
|
|
|
46
62
|
versionCheckInterval: (_e = config.versionCheckInterval) != null ? _e : 36e5,
|
|
47
63
|
// 1 hour
|
|
48
64
|
onUpdateAvailable: config.onUpdateAvailable,
|
|
49
|
-
onUpdateRequired: config.onUpdateRequired
|
|
65
|
+
onUpdateRequired: config.onUpdateRequired,
|
|
66
|
+
debugMode: (_f = config.debugMode) != null ? _f : false,
|
|
67
|
+
environment: config.environment
|
|
50
68
|
};
|
|
51
69
|
this.loadDismissedMessages();
|
|
52
70
|
}
|
|
@@ -54,23 +72,34 @@ var _SiteManager = class _SiteManager {
|
|
|
54
72
|
* Initialize and start the site manager
|
|
55
73
|
*/
|
|
56
74
|
async init() {
|
|
75
|
+
var _a;
|
|
57
76
|
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
58
77
|
console.warn(
|
|
59
78
|
"Featurely Site Manager: Can only be initialized in a browser environment"
|
|
60
79
|
);
|
|
61
80
|
return;
|
|
62
81
|
}
|
|
63
|
-
|
|
64
|
-
|
|
82
|
+
if (this.config.debugMode) {
|
|
83
|
+
this.setupDebugOverlay();
|
|
84
|
+
}
|
|
85
|
+
this.debugLog("info", `[init] v1.1.14 | project: ${this.config.projectId} | env: ${(_a = this.config.environment) != null ? _a : "\u2014"}`);
|
|
86
|
+
this.debugLog("info", `[init] analytics: ${this.config.enableAnalytics ? "on" : "off"} | poll: ${this.config.pollInterval}ms | debug: ${this.config.debugMode ? "on" : "off"}`);
|
|
65
87
|
if (this.config.enableAnalytics) {
|
|
88
|
+
this.debugLog("info", "[init] starting analytics + page tracking");
|
|
66
89
|
this.startAnalyticsFlushing();
|
|
67
90
|
this.setupPageTracking();
|
|
68
91
|
}
|
|
92
|
+
this.debugLog("info", "[init] fetching initial config\u2026");
|
|
93
|
+
await this.fetchConfig();
|
|
94
|
+
this.startPolling();
|
|
95
|
+
this.debugLog("info", `[init] polling every ${this.config.pollInterval / 1e3}s`);
|
|
69
96
|
if (this.config.enableVersionCheck && this.config.appVersion) {
|
|
97
|
+
this.debugLog("info", `[init] checking version (current: ${this.config.appVersion})`);
|
|
70
98
|
await this.checkVersion();
|
|
71
99
|
this.startVersionChecking();
|
|
72
100
|
}
|
|
73
101
|
this.injectStyles();
|
|
102
|
+
this.debugLog("info", "[init] \u2713 ready");
|
|
74
103
|
}
|
|
75
104
|
/**
|
|
76
105
|
* Stop the site manager and clean up
|
|
@@ -80,6 +109,7 @@ var _SiteManager = class _SiteManager {
|
|
|
80
109
|
this.stopVersionChecking();
|
|
81
110
|
this.stopAnalyticsFlushing();
|
|
82
111
|
this.flushAnalytics();
|
|
112
|
+
this.stopDebugOverlay();
|
|
83
113
|
this.clearMessages();
|
|
84
114
|
}
|
|
85
115
|
/**
|
|
@@ -115,6 +145,7 @@ var _SiteManager = class _SiteManager {
|
|
|
115
145
|
return false;
|
|
116
146
|
}
|
|
117
147
|
const isEnabled = this.evaluateFeatureFlag(flag);
|
|
148
|
+
this.debugLog("info", `[flag] "${flagKey}" \u2192 ${isEnabled ? "ENABLED" : "DISABLED"}`);
|
|
118
149
|
const trackKey = `feature_flag_tracked_${flagKey}`;
|
|
119
150
|
if (typeof sessionStorage !== "undefined" && !sessionStorage.getItem(trackKey)) {
|
|
120
151
|
this.trackEvent("feature_flag_evaluated", {
|
|
@@ -238,6 +269,9 @@ var _SiteManager = class _SiteManager {
|
|
|
238
269
|
properties,
|
|
239
270
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
240
271
|
});
|
|
272
|
+
this.recentAnalyticsEvents.unshift({ name: eventName, ts: (/* @__PURE__ */ new Date()).toLocaleTimeString() });
|
|
273
|
+
if (this.recentAnalyticsEvents.length > 50) this.recentAnalyticsEvents.length = 50;
|
|
274
|
+
this.debugLog("info", `trackEvent: ${eventName}`);
|
|
241
275
|
if (this.analyticsQueue.length >= 10) {
|
|
242
276
|
this.flushAnalytics();
|
|
243
277
|
}
|
|
@@ -246,8 +280,9 @@ var _SiteManager = class _SiteManager {
|
|
|
246
280
|
// Configuration Fetching
|
|
247
281
|
// ============================================================================
|
|
248
282
|
async fetchConfig() {
|
|
249
|
-
var _a, _b;
|
|
283
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
250
284
|
try {
|
|
285
|
+
const fetchStartMs = Date.now();
|
|
251
286
|
const response = await fetch(
|
|
252
287
|
`${this.config.apiUrl}/api/public/v1/site-config?projectId=${this.config.projectId}`,
|
|
253
288
|
{
|
|
@@ -256,6 +291,7 @@ var _SiteManager = class _SiteManager {
|
|
|
256
291
|
}
|
|
257
292
|
}
|
|
258
293
|
);
|
|
294
|
+
this.debugNetwork(`/api/public/v1/site-config`, response.status, Date.now() - fetchStartMs);
|
|
259
295
|
if (!response.ok) {
|
|
260
296
|
const errorData = await response.json().catch(() => ({}));
|
|
261
297
|
const message = errorData.error || response.statusText;
|
|
@@ -278,9 +314,30 @@ var _SiteManager = class _SiteManager {
|
|
|
278
314
|
const configChanged = JSON.stringify(newConfig) !== JSON.stringify(this.siteConfig);
|
|
279
315
|
this.consecutiveFetchFailures = 0;
|
|
280
316
|
if (configChanged) {
|
|
281
|
-
const
|
|
282
|
-
const
|
|
317
|
+
const prevMaintenance = (_b = (_a = this.siteConfig) == null ? void 0 : _a.maintenance.enabled) != null ? _b : false;
|
|
318
|
+
const prevFlagCount = (_d = (_c = this.siteConfig) == null ? void 0 : _c.featureFlags.length) != null ? _d : 0;
|
|
319
|
+
const prevMsgCount = (_f = (_e = this.siteConfig) == null ? void 0 : _e.messages.length) != null ? _f : 0;
|
|
320
|
+
const delta = [];
|
|
321
|
+
if (newConfig.maintenance.enabled !== prevMaintenance)
|
|
322
|
+
delta.push(`maintenance ${newConfig.maintenance.enabled ? "ON" : "OFF"}`);
|
|
323
|
+
if (newConfig.featureFlags.length !== prevFlagCount)
|
|
324
|
+
delta.push(`flags: ${prevFlagCount}\u2192${newConfig.featureFlags.length}`);
|
|
325
|
+
if (newConfig.messages.length !== prevMsgCount)
|
|
326
|
+
delta.push(`messages: ${prevMsgCount}\u2192${newConfig.messages.length}`);
|
|
327
|
+
this.debugLog("info", `[config] updated${delta.length ? ": " + delta.join(", ") : " (structure changed)"}`);
|
|
328
|
+
const wasMaintenanceEnabled = (_g = this.siteConfig) == null ? void 0 : _g.maintenance.enabled;
|
|
329
|
+
const oldFeatureFlags = ((_h = this.siteConfig) == null ? void 0 : _h.featureFlags) || [];
|
|
283
330
|
this.siteConfig = newConfig;
|
|
331
|
+
if (newConfig.debugMode === true && !this.debugOverlayEl) {
|
|
332
|
+
this.setupDebugOverlay();
|
|
333
|
+
}
|
|
334
|
+
if (!this.debugOverlayEl && this.config.environment) {
|
|
335
|
+
const envs = newConfig.environments;
|
|
336
|
+
const env = envs == null ? void 0 : envs.find((e) => e.slug === this.config.environment || e.name === this.config.environment);
|
|
337
|
+
if ((env == null ? void 0 : env.debugEnabled) === true) {
|
|
338
|
+
this.setupDebugOverlay();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
284
341
|
if (newConfig.maintenance.enabled && !wasMaintenanceEnabled) {
|
|
285
342
|
this.enableMaintenanceMode();
|
|
286
343
|
} else if (!newConfig.maintenance.enabled && wasMaintenanceEnabled) {
|
|
@@ -294,17 +351,22 @@ var _SiteManager = class _SiteManager {
|
|
|
294
351
|
}
|
|
295
352
|
}
|
|
296
353
|
this.updateMessages();
|
|
354
|
+
} else {
|
|
355
|
+
this.debugLog("info", "[config] polled \u2014 no changes");
|
|
297
356
|
}
|
|
298
357
|
} catch (error) {
|
|
299
358
|
this.consecutiveFetchFailures++;
|
|
359
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
360
|
+
this.debugLog("error", `[config] fetch failed (attempt ${this.consecutiveFetchFailures}): ${errMsg}`);
|
|
361
|
+
this.errorCount++;
|
|
300
362
|
if (this.consecutiveFetchFailures <= _SiteManager.MAX_CONSECUTIVE_FAILURES) {
|
|
301
363
|
const isNetworkError = error instanceof TypeError && (error.message.includes("NetworkError") || error.message.includes("Failed to fetch") || error.message.includes("fetch"));
|
|
302
364
|
if (isNetworkError) {
|
|
303
|
-
|
|
304
|
-
`[Featurely] Network error \u2014 request to Featurely was blocked.
|
|
365
|
+
const cspMsg = `[Featurely] Network error \u2014 request to Featurely was blocked.
|
|
305
366
|
\u2192 If your site has a Content-Security-Policy, add 'https://www.featurely.no' to the connect-src directive.
|
|
306
|
-
Example: connect-src 'self' https://www.featurely.no
|
|
307
|
-
);
|
|
367
|
+
Example: connect-src 'self' https://www.featurely.no ...`;
|
|
368
|
+
console.error(cspMsg);
|
|
369
|
+
this.debugLog("error", "[config] possible CSP block \u2014 check connect-src");
|
|
308
370
|
} else {
|
|
309
371
|
console.error("Featurely Site Manager: Failed to fetch configuration", error);
|
|
310
372
|
}
|
|
@@ -312,9 +374,9 @@ var _SiteManager = class _SiteManager {
|
|
|
312
374
|
this.config.onError(error);
|
|
313
375
|
}
|
|
314
376
|
} else if (this.consecutiveFetchFailures === _SiteManager.MAX_CONSECUTIVE_FAILURES + 1) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
);
|
|
377
|
+
const silenceMsg = `Featurely Site Manager: Silencing repeated fetch errors after ${_SiteManager.MAX_CONSECUTIVE_FAILURES} failures. Check your apiUrl and Content-Security-Policy configuration.`;
|
|
378
|
+
console.error(silenceMsg);
|
|
379
|
+
this.debugLog("error", `[config] silencing errors after ${_SiteManager.MAX_CONSECUTIVE_FAILURES} consecutive failures`);
|
|
318
380
|
}
|
|
319
381
|
}
|
|
320
382
|
}
|
|
@@ -352,6 +414,8 @@ var _SiteManager = class _SiteManager {
|
|
|
352
414
|
const url = `${this.config.apiUrl}/api/projects/${this.config.projectId}/analytics/events`;
|
|
353
415
|
const eventsToSend = [...this.analyticsQueue];
|
|
354
416
|
this.analyticsQueue = [];
|
|
417
|
+
this.analyticsEventsSent += eventsToSend.length;
|
|
418
|
+
this.debugLog("info", `[analytics] flushing ${eventsToSend.length} event(s) (total sent: ${this.analyticsEventsSent})`);
|
|
355
419
|
for (const event of eventsToSend) {
|
|
356
420
|
const payload = JSON.stringify({
|
|
357
421
|
eventName: event.eventName,
|
|
@@ -368,25 +432,364 @@ var _SiteManager = class _SiteManager {
|
|
|
368
432
|
}
|
|
369
433
|
} else {
|
|
370
434
|
try {
|
|
435
|
+
const t0 = Date.now();
|
|
371
436
|
const res = await fetch(url, {
|
|
372
437
|
method: "POST",
|
|
373
438
|
headers: { "Content-Type": "application/json" },
|
|
374
439
|
body: payload,
|
|
375
440
|
keepalive: true
|
|
376
441
|
});
|
|
442
|
+
this.debugNetwork(`/api/projects/${this.config.projectId}/analytics/events`, res.status, Date.now() - t0);
|
|
377
443
|
if (!res.ok) {
|
|
378
444
|
const body = await res.text().catch(() => "(unreadable)");
|
|
379
|
-
|
|
445
|
+
const errDetail = `[analytics] event "${event.eventName}" rejected: ${res.status} ${res.statusText} \u2014 ${body}`;
|
|
446
|
+
console.error(`[Featurely] ${errDetail}`);
|
|
447
|
+
this.debugLog("error", errDetail);
|
|
448
|
+
this.errorCount++;
|
|
380
449
|
}
|
|
381
450
|
} catch (fetchError) {
|
|
382
451
|
const sentViaBeacon = typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && navigator.sendBeacon(url, payload);
|
|
383
452
|
if (!sentViaBeacon) {
|
|
453
|
+
const errMsg = fetchError instanceof Error ? fetchError.message : String(fetchError);
|
|
384
454
|
console.error(`[Featurely] Failed to send analytics event "${event.eventName}":`, fetchError);
|
|
455
|
+
this.debugLog("error", `[analytics] send failed for "${event.eventName}": ${errMsg}`);
|
|
456
|
+
this.errorCount++;
|
|
457
|
+
} else {
|
|
458
|
+
this.debugLog("warn", `[analytics] fetch failed for "${event.eventName}", delivered via sendBeacon`);
|
|
385
459
|
}
|
|
386
460
|
}
|
|
387
461
|
}
|
|
388
462
|
}
|
|
389
463
|
}
|
|
464
|
+
/** Log a message to the debug panel (max 100 entries). */
|
|
465
|
+
debugLog(level, msg) {
|
|
466
|
+
if (!this.config.debugMode && !this.debugOverlayEl) return;
|
|
467
|
+
const ts = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
468
|
+
this.debugLogs.unshift({ ts, level, msg });
|
|
469
|
+
if (this.debugLogs.length > 100) this.debugLogs.length = 100;
|
|
470
|
+
}
|
|
471
|
+
/** Add a network call entry. */
|
|
472
|
+
debugNetwork(url, status, ms) {
|
|
473
|
+
if (!this.config.debugMode && !this.debugOverlayEl) return;
|
|
474
|
+
const ts = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
475
|
+
const shortUrl = url.replace(/^https?:\/\/[^/]+/, "");
|
|
476
|
+
this.networkLog.unshift({ url: shortUrl, status, ms, ts });
|
|
477
|
+
if (this.networkLog.length > 50) this.networkLog.length = 50;
|
|
478
|
+
}
|
|
479
|
+
stopDebugOverlay() {
|
|
480
|
+
var _a;
|
|
481
|
+
if (this.debugRefreshId) {
|
|
482
|
+
clearInterval(this.debugRefreshId);
|
|
483
|
+
this.debugRefreshId = null;
|
|
484
|
+
}
|
|
485
|
+
if ((_a = this.debugOverlayEl) == null ? void 0 : _a.parentNode) {
|
|
486
|
+
this.debugOverlayEl.parentNode.removeChild(this.debugOverlayEl);
|
|
487
|
+
}
|
|
488
|
+
this.debugOverlayEl = null;
|
|
489
|
+
if (this.originalConsoleError) {
|
|
490
|
+
console.error = this.originalConsoleError;
|
|
491
|
+
this.originalConsoleError = null;
|
|
492
|
+
}
|
|
493
|
+
if (this.originalConsoleWarn) {
|
|
494
|
+
console.warn = this.originalConsoleWarn;
|
|
495
|
+
this.originalConsoleWarn = null;
|
|
496
|
+
}
|
|
497
|
+
this.consoleIntercepted = false;
|
|
498
|
+
}
|
|
499
|
+
setupGlobalErrorCapture() {
|
|
500
|
+
if (this.consoleIntercepted) return;
|
|
501
|
+
this.consoleIntercepted = true;
|
|
502
|
+
this.originalConsoleError = console.error.bind(console);
|
|
503
|
+
console.error = (...args) => {
|
|
504
|
+
this.originalConsoleError(...args);
|
|
505
|
+
const msg = args.map((a) => typeof a === "string" ? a : a instanceof Error ? `${a.message}` : JSON.stringify(a)).join(" ");
|
|
506
|
+
this.errorCount++;
|
|
507
|
+
this.debugLog("error", `[console.error] ${msg.slice(0, 300)}`);
|
|
508
|
+
};
|
|
509
|
+
this.originalConsoleWarn = console.warn.bind(console);
|
|
510
|
+
console.warn = (...args) => {
|
|
511
|
+
this.originalConsoleWarn(...args);
|
|
512
|
+
const msg = args.map((a) => typeof a === "string" ? a : a instanceof Error ? `${a.message}` : JSON.stringify(a)).join(" ");
|
|
513
|
+
this.debugLog("warn", `[console.warn] ${msg.slice(0, 300)}`);
|
|
514
|
+
};
|
|
515
|
+
window.addEventListener("error", (e) => {
|
|
516
|
+
this.errorCount++;
|
|
517
|
+
const src = e.filename ? `${e.filename.replace(/.*\//, "")}:${e.lineno}:${e.colno}` : "";
|
|
518
|
+
this.debugLog("error", `[uncaught error] ${e.message}${src ? ` @ ${src}` : ""}`);
|
|
519
|
+
});
|
|
520
|
+
window.addEventListener("unhandledrejection", (e) => {
|
|
521
|
+
this.errorCount++;
|
|
522
|
+
const reason = e.reason instanceof Error ? e.reason.message : typeof e.reason === "string" ? e.reason : JSON.stringify(e.reason);
|
|
523
|
+
this.debugLog("error", `[unhandled promise] ${reason.slice(0, 300)}`);
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
setupDebugOverlay() {
|
|
527
|
+
if (typeof document === "undefined" || this.debugOverlayEl) return;
|
|
528
|
+
const el = document.createElement("div");
|
|
529
|
+
el.id = "__featurely_debug__";
|
|
530
|
+
el.setAttribute("data-featurely-debug", "true");
|
|
531
|
+
const BG = "rgba(15,15,20,0.97)";
|
|
532
|
+
const BORDER = "rgba(255,255,255,0.12)";
|
|
533
|
+
const TEXT = "#e5e7eb";
|
|
534
|
+
const W = "360px";
|
|
535
|
+
Object.assign(el.style, {
|
|
536
|
+
position: "fixed",
|
|
537
|
+
bottom: "16px",
|
|
538
|
+
right: "16px",
|
|
539
|
+
width: W,
|
|
540
|
+
zIndex: "2147483647",
|
|
541
|
+
fontFamily: "'SF Mono', 'Fira Code', 'Cascadia Code', monospace",
|
|
542
|
+
fontSize: "11px",
|
|
543
|
+
lineHeight: "1.5",
|
|
544
|
+
color: TEXT,
|
|
545
|
+
background: BG,
|
|
546
|
+
border: `1px solid ${BORDER}`,
|
|
547
|
+
borderRadius: "10px",
|
|
548
|
+
boxShadow: "0 8px 32px rgba(0,0,0,0.5)",
|
|
549
|
+
overflow: "hidden",
|
|
550
|
+
userSelect: "none",
|
|
551
|
+
transition: "height 0.15s ease"
|
|
552
|
+
});
|
|
553
|
+
window.__FEATURELY_DEBUG__ = {
|
|
554
|
+
log: (pkg, level, msg) => {
|
|
555
|
+
this.debugLog(level, `[${pkg}] ${msg}`);
|
|
556
|
+
},
|
|
557
|
+
registerPackage: (pkg, getState) => {
|
|
558
|
+
this[`__debug_${pkg}`] = getState;
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
document.body.appendChild(el);
|
|
562
|
+
this.debugOverlayEl = el;
|
|
563
|
+
this.setupGlobalErrorCapture();
|
|
564
|
+
this.debugLog("info", `[site-manager] debug overlay initialized`);
|
|
565
|
+
this.debugLog("info", `[site-manager] v1.1.14 | project: ${this.config.projectId}`);
|
|
566
|
+
this.renderDebugOverlay();
|
|
567
|
+
this.debugRefreshId = setInterval(() => this.renderDebugOverlay(), 1500);
|
|
568
|
+
}
|
|
569
|
+
renderDebugOverlay() {
|
|
570
|
+
var _a, _b, _c, _d, _e;
|
|
571
|
+
const el = this.debugOverlayEl;
|
|
572
|
+
if (!el) return;
|
|
573
|
+
const ACCENT = "#6366f1";
|
|
574
|
+
const BORDER = "rgba(255,255,255,0.12)";
|
|
575
|
+
const TEXT = "#e5e7eb";
|
|
576
|
+
const MUTED = "#9ca3af";
|
|
577
|
+
const GREEN = "#22c55e";
|
|
578
|
+
const RED = "#ef4444";
|
|
579
|
+
const YELLOW = "#eab308";
|
|
580
|
+
const tab = this.debugTab;
|
|
581
|
+
const minimized = this.debugMinimized;
|
|
582
|
+
const cfg = this.config;
|
|
583
|
+
const sc = this.siteConfig;
|
|
584
|
+
const tabBtn = (id, label, badgeCount) => {
|
|
585
|
+
const badgeHtml = badgeCount && badgeCount > 0 ? `<span style="display:inline-flex;align-items:center;justify-content:center;min-width:14px;height:14px;padding:0 3px;margin-left:3px;border-radius:9999px;font-size:8px;font-weight:700;background:${RED};color:#fff;vertical-align:middle">${badgeCount > 9 ? "9+" : badgeCount}</span>` : "";
|
|
586
|
+
return `<button data-tab="${id}" style="padding:3px 10px;border-radius:4px;border:none;cursor:pointer;font-size:10px;font-family:inherit;background:${tab === id ? ACCENT : "rgba(255,255,255,0.06)"};color:${tab === id ? "#fff" : MUTED};transition:background 0.1s">${label}${badgeHtml}</button>`;
|
|
587
|
+
};
|
|
588
|
+
const badge = (text, color) => `<span style="padding:1px 6px;border-radius:9999px;font-size:9px;font-weight:600;background:${color}22;color:${color}">${text}</span>`;
|
|
589
|
+
const row = (label, value) => `<div style="display:flex;justify-content:space-between;align-items:center;padding:3px 0;border-bottom:1px solid rgba(255,255,255,0.04)"><span style="color:${MUTED}">${label}</span><span style="color:${TEXT};word-break:break-all;text-align:right;max-width:220px">${value}</span></div>`;
|
|
590
|
+
let content = "";
|
|
591
|
+
if (!minimized) {
|
|
592
|
+
if (tab === "sdk") {
|
|
593
|
+
const maintenance = ((_a = sc == null ? void 0 : sc.maintenance) == null ? void 0 : _a.enabled) ? badge("ON", RED) : badge("OFF", GREEN);
|
|
594
|
+
const flagCount = (_c = (_b = sc == null ? void 0 : sc.featureFlags) == null ? void 0 : _b.length) != null ? _c : 0;
|
|
595
|
+
const msgCount = (_e = (_d = sc == null ? void 0 : sc.messages) == null ? void 0 : _d.length) != null ? _e : 0;
|
|
596
|
+
content = `
|
|
597
|
+
${row("SDK Version", "1.1.14")}
|
|
598
|
+
${row("Project", cfg.projectId)}
|
|
599
|
+
${row("Environment", cfg.environment || "\u2014")}
|
|
600
|
+
${row("API URL", cfg.apiUrl)}
|
|
601
|
+
${row("Maintenance", maintenance)}
|
|
602
|
+
${row("Feature Flags", `${flagCount} active`)}
|
|
603
|
+
${row("Status Messages", `${msgCount} active`)}
|
|
604
|
+
${row("Analytics Queue", `${this.analyticsQueue.length} pending`)}
|
|
605
|
+
${row("Events Sent", `${this.analyticsEventsSent}`)}
|
|
606
|
+
${row("Session ID", this.sessionId.slice(0, 16) + "\u2026")}
|
|
607
|
+
${row("User ID", cfg.userId || "\u2014")}
|
|
608
|
+
${row("Page", this.currentPagePath || "\u2014")}
|
|
609
|
+
`;
|
|
610
|
+
const pkgKeys = Object.keys(this).filter((k) => k.startsWith("__debug_"));
|
|
611
|
+
for (const k of pkgKeys) {
|
|
612
|
+
const fn = this[k];
|
|
613
|
+
if (typeof fn === "function") {
|
|
614
|
+
try {
|
|
615
|
+
content += `<div style="margin-top:6px;padding-top:4px;border-top:1px solid ${BORDER}">${fn()}</div>`;
|
|
616
|
+
} catch {
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
} else if (tab === "logs") {
|
|
621
|
+
const logs = this.debugLogs.slice(0, 50);
|
|
622
|
+
const errorsInLog = logs.filter((l) => l.level === "error").length;
|
|
623
|
+
const clearBtn = `<button data-action="clear-logs" style="background:rgba(255,255,255,0.08);border:none;color:${MUTED};cursor:pointer;font-size:9px;font-family:inherit;padding:2px 7px;border-radius:3px">Clear</button>`;
|
|
624
|
+
const errBadge = errorsInLog > 0 ? `<span style="font-size:9px;color:${RED}">${errorsInLog} error${errorsInLog > 1 ? "s" : ""}</span>` : "";
|
|
625
|
+
const logHeader = `<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">${errBadge || `<span></span>`}${clearBtn}</div>`;
|
|
626
|
+
if (logs.length === 0) {
|
|
627
|
+
content = logHeader + `<div style="color:${MUTED};padding:12px;text-align:center">No logs yet</div>`;
|
|
628
|
+
} else {
|
|
629
|
+
content = logHeader + logs.map((l) => {
|
|
630
|
+
const c = l.level === "error" ? RED : l.level === "warn" ? YELLOW : "#a5b4fc";
|
|
631
|
+
return `<div style="padding:2px 0;display:flex;gap:6px;align-items:flex-start;border-bottom:1px solid rgba(255,255,255,0.03)"><span style="color:${MUTED};white-space:nowrap;font-size:9px">${l.ts}</span><span style="color:${c};flex:1;word-break:break-word">${l.msg}</span></div>`;
|
|
632
|
+
}).join("");
|
|
633
|
+
}
|
|
634
|
+
} else if (tab === "network") {
|
|
635
|
+
const calls = this.networkLog.slice(0, 20);
|
|
636
|
+
if (calls.length === 0) {
|
|
637
|
+
content = `<div style="color:${MUTED};padding:16px;text-align:center">No network calls yet</div>`;
|
|
638
|
+
} else {
|
|
639
|
+
content = calls.map((n) => {
|
|
640
|
+
const ok = typeof n.status === "number" && n.status >= 200 && n.status < 300;
|
|
641
|
+
const sc2 = ok ? GREEN : RED;
|
|
642
|
+
return `<div style="padding:3px 0;border-bottom:1px solid rgba(255,255,255,0.03)"><div style="display:flex;justify-content:space-between"><span style="color:${sc2};font-weight:600">${n.status}</span><span style="color:${MUTED}">${n.ms}ms</span><span style="color:${MUTED}">${n.ts}</span></div><div style="color:${TEXT};word-break:break-all">${n.url}</div></div>`;
|
|
643
|
+
}).join("");
|
|
644
|
+
}
|
|
645
|
+
} else if (tab === "events") {
|
|
646
|
+
const events = this.recentAnalyticsEvents.slice(0, 30);
|
|
647
|
+
if (events.length === 0) {
|
|
648
|
+
content = `<div style="color:${MUTED};padding:16px;text-align:center">No events yet</div>`;
|
|
649
|
+
} else {
|
|
650
|
+
content = events.map(
|
|
651
|
+
(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>`
|
|
652
|
+
).join("");
|
|
653
|
+
}
|
|
654
|
+
} else if (tab === "test") {
|
|
655
|
+
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`;
|
|
656
|
+
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>`;
|
|
657
|
+
const feedbackHtml = this.testFeedback.slice(0, 6).map(
|
|
658
|
+
(f) => `<div style="display:flex;gap:6px;align-items:center;padding:2px 0;border-bottom:1px solid rgba(255,255,255,0.03)">
|
|
659
|
+
<span style="color:${f.ok ? GREEN : RED};font-size:10px">●</span>
|
|
660
|
+
<span style="color:${TEXT};flex:1">${f.msg}</span>
|
|
661
|
+
<span style="color:${MUTED}">${f.ts}</span>
|
|
662
|
+
</div>`
|
|
663
|
+
).join("");
|
|
664
|
+
content = `
|
|
665
|
+
${sectionLabel("Analytics")}
|
|
666
|
+
<div style="display:flex;gap:4px;flex-wrap:wrap">
|
|
667
|
+
<button data-action="test-event" style="${btnStyle(ACCENT)}">Send test event</button>
|
|
668
|
+
<button data-action="flush" style="${btnStyle("rgba(255,255,255,0.15)")}">Flush queue now</button>
|
|
669
|
+
</div>
|
|
670
|
+
${sectionLabel("Feature Flags")}
|
|
671
|
+
<div style="display:flex;gap:4px;align-items:center">
|
|
672
|
+
<input data-testinput="flagkey" value="${this.testFlagKey.replace(/"/g, """)}" placeholder="flag-key" style="flex:1;background:rgba(255,255,255,0.07);border:1px solid ${BORDER};border-radius:4px;color:${TEXT};font-family:inherit;font-size:10px;padding:3px 7px;outline:none" />
|
|
673
|
+
<button data-action="check-flag" style="${btnStyle(ACCENT)}">Check</button>
|
|
674
|
+
</div>
|
|
675
|
+
${sectionLabel("Errors & Bugs")}
|
|
676
|
+
<div style="display:flex;gap:4px;flex-wrap:wrap">
|
|
677
|
+
<button data-action="test-error" style="${btnStyle(RED)}">Send test error</button>
|
|
678
|
+
<button data-action="test-bug" style="${btnStyle(YELLOW)}">Send test bug</button>
|
|
679
|
+
</div>
|
|
680
|
+
${sectionLabel("Config")}
|
|
681
|
+
<div style="display:flex;gap:4px;flex-wrap:wrap">
|
|
682
|
+
<button data-action="refresh" style="${btnStyle("rgba(255,255,255,0.15)")}">Refresh config</button>
|
|
683
|
+
<button data-action="clear-logs" style="${btnStyle("rgba(255,255,255,0.15)")}">Clear logs</button>
|
|
684
|
+
</div>
|
|
685
|
+
${this.testFeedback.length > 0 ? `
|
|
686
|
+
<div style="margin-top:10px;padding-top:6px;border-top:1px solid ${BORDER}">${feedbackHtml}</div>
|
|
687
|
+
` : ""}
|
|
688
|
+
`;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
el.innerHTML = `
|
|
692
|
+
<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__">
|
|
693
|
+
<span style="color:${ACCENT};font-weight:700;font-size:10px;letter-spacing:0.05em">⬡ FEATURELY DEBUG</span>
|
|
694
|
+
<span style="flex:1"></span>
|
|
695
|
+
${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>`}
|
|
696
|
+
<button id="__ft_dbg_min__" style="background:none;border:none;color:${MUTED};cursor:pointer;font-size:14px;line-height:1;padding:0 2px" title="${minimized ? "Expand" : "Minimize"}">${minimized ? "⬆" : "⬇"}</button>
|
|
697
|
+
<button id="__ft_dbg_cls__" style="background:none;border:none;color:${MUTED};cursor:pointer;font-size:14px;line-height:1;padding:0 2px" title="Close">×</button>
|
|
698
|
+
</div>
|
|
699
|
+
${minimized ? "" : `<div style="padding:8px 10px;max-height:300px;overflow-y:auto">${content}</div>`}
|
|
700
|
+
`;
|
|
701
|
+
el.querySelectorAll("[data-tab]").forEach((btn) => {
|
|
702
|
+
btn.onclick = (e) => {
|
|
703
|
+
e.stopPropagation();
|
|
704
|
+
this.debugTab = btn.dataset.tab;
|
|
705
|
+
this.renderDebugOverlay();
|
|
706
|
+
};
|
|
707
|
+
});
|
|
708
|
+
const minBtn = el.querySelector("#__ft_dbg_min__");
|
|
709
|
+
if (minBtn) {
|
|
710
|
+
minBtn.onclick = (e) => {
|
|
711
|
+
e.stopPropagation();
|
|
712
|
+
this.debugMinimized = !this.debugMinimized;
|
|
713
|
+
this.renderDebugOverlay();
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
const clsBtn = el.querySelector("#__ft_dbg_cls__");
|
|
717
|
+
if (clsBtn) {
|
|
718
|
+
clsBtn.onclick = (e) => {
|
|
719
|
+
e.stopPropagation();
|
|
720
|
+
this.stopDebugOverlay();
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
el.querySelectorAll("[data-action]").forEach((btn) => {
|
|
724
|
+
btn.onclick = (e) => {
|
|
725
|
+
e.stopPropagation();
|
|
726
|
+
void this.handleTestAction(btn.dataset.action);
|
|
727
|
+
};
|
|
728
|
+
});
|
|
729
|
+
el.querySelectorAll("[data-testinput='flagkey']").forEach((input) => {
|
|
730
|
+
input.oninput = (e) => {
|
|
731
|
+
e.stopPropagation();
|
|
732
|
+
this.testFlagKey = e.target.value;
|
|
733
|
+
};
|
|
734
|
+
input.onclick = (e) => e.stopPropagation();
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
async handleTestAction(action) {
|
|
738
|
+
const ts = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
739
|
+
const feedback = (msg, ok) => {
|
|
740
|
+
this.testFeedback.unshift({ msg, ok, ts });
|
|
741
|
+
if (this.testFeedback.length > 10) this.testFeedback.length = 10;
|
|
742
|
+
this.renderDebugOverlay();
|
|
743
|
+
};
|
|
744
|
+
switch (action) {
|
|
745
|
+
case "test-event":
|
|
746
|
+
this.trackEvent("debug_test_event", { source: "debug_panel", ts: (/* @__PURE__ */ new Date()).toISOString() });
|
|
747
|
+
feedback("debug_test_event queued", true);
|
|
748
|
+
break;
|
|
749
|
+
case "flush": {
|
|
750
|
+
const before = this.analyticsQueue.length;
|
|
751
|
+
await this.flushAnalytics();
|
|
752
|
+
feedback(`Flushed ${before} event(s)`, true);
|
|
753
|
+
break;
|
|
754
|
+
}
|
|
755
|
+
case "check-flag": {
|
|
756
|
+
const key = this.testFlagKey.trim();
|
|
757
|
+
if (!key) {
|
|
758
|
+
feedback("Enter a flag key first", false);
|
|
759
|
+
break;
|
|
760
|
+
}
|
|
761
|
+
const enabled = this.isFeatureEnabled(key);
|
|
762
|
+
feedback(`"${key}" \u2192 ${enabled ? "ENABLED \u2713" : "DISABLED \u2717"}`, enabled);
|
|
763
|
+
break;
|
|
764
|
+
}
|
|
765
|
+
case "test-error":
|
|
766
|
+
this.trackEvent("error_reported", {
|
|
767
|
+
message: "Test error from debug panel",
|
|
768
|
+
source: "debug_panel",
|
|
769
|
+
severity: "error"
|
|
770
|
+
});
|
|
771
|
+
feedback("error_reported queued", true);
|
|
772
|
+
break;
|
|
773
|
+
case "test-bug":
|
|
774
|
+
this.trackEvent("bug_reported", {
|
|
775
|
+
title: "Test bug from debug panel",
|
|
776
|
+
source: "debug_panel",
|
|
777
|
+
severity: "medium"
|
|
778
|
+
});
|
|
779
|
+
feedback("bug_reported queued", true);
|
|
780
|
+
break;
|
|
781
|
+
case "refresh":
|
|
782
|
+
await this.fetchConfig();
|
|
783
|
+
feedback("Config refreshed", true);
|
|
784
|
+
break;
|
|
785
|
+
case "clear-logs":
|
|
786
|
+
this.debugLogs = [];
|
|
787
|
+
this.testFeedback = [];
|
|
788
|
+
this.errorCount = 0;
|
|
789
|
+
this.renderDebugOverlay();
|
|
790
|
+
break;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
390
793
|
setupPageTracking() {
|
|
391
794
|
if (this.pageTrackingSetup) return;
|
|
392
795
|
this.pageTrackingSetup = true;
|
|
@@ -634,8 +1037,10 @@ var _SiteManager = class _SiteManager {
|
|
|
634
1037
|
enableMaintenanceMode() {
|
|
635
1038
|
if (!this.siteConfig) return;
|
|
636
1039
|
if (this.shouldBypassMaintenance()) {
|
|
1040
|
+
this.debugLog("info", "[maintenance] ENABLED \u2014 user bypassed (whitelist match)");
|
|
637
1041
|
return;
|
|
638
1042
|
}
|
|
1043
|
+
this.debugLog("warn", `[maintenance] ENABLED (type: ${this.siteConfig.maintenance.type})`);
|
|
639
1044
|
this.showMaintenancePage();
|
|
640
1045
|
this.trackEvent("maintenance_enabled", {
|
|
641
1046
|
maintenanceType: this.siteConfig.maintenance.type
|
|
@@ -645,6 +1050,7 @@ var _SiteManager = class _SiteManager {
|
|
|
645
1050
|
}
|
|
646
1051
|
}
|
|
647
1052
|
disableMaintenanceMode() {
|
|
1053
|
+
this.debugLog("info", "[maintenance] DISABLED \u2014 site back online");
|
|
648
1054
|
this.hideMaintenancePage();
|
|
649
1055
|
this.trackEvent("maintenance_disabled");
|
|
650
1056
|
if (this.config.onMaintenanceDisabled) {
|
|
@@ -739,6 +1145,7 @@ var _SiteManager = class _SiteManager {
|
|
|
739
1145
|
// Status Messages
|
|
740
1146
|
// ============================================================================
|
|
741
1147
|
updateMessages() {
|
|
1148
|
+
var _a, _b;
|
|
742
1149
|
if (!this.siteConfig) return;
|
|
743
1150
|
const currentTime = /* @__PURE__ */ new Date();
|
|
744
1151
|
const currentPath = window.location.pathname;
|
|
@@ -773,8 +1180,10 @@ var _SiteManager = class _SiteManager {
|
|
|
773
1180
|
this.messageContainers.delete(id);
|
|
774
1181
|
}
|
|
775
1182
|
});
|
|
1183
|
+
this.debugLog("info", `[messages] ${activeMessages.length} active (${(_b = (_a = this.siteConfig) == null ? void 0 : _a.messages.length) != null ? _b : 0} total)`);
|
|
776
1184
|
activeMessages.forEach((message) => {
|
|
777
1185
|
if (!this.messageContainers.has(message.id)) {
|
|
1186
|
+
this.debugLog("info", `[messages] showing "${message.title}" (${message.type}, ${message.style})`);
|
|
778
1187
|
this.showMessage(message);
|
|
779
1188
|
if (this.config.onMessageReceived) {
|
|
780
1189
|
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.14",
|
|
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",
|