@tracelog/lib 0.6.0 → 0.6.3
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/README.md +9 -9
- package/dist/browser/tracelog.esm.js +406 -304
- package/dist/browser/tracelog.esm.js.map +1 -0
- package/dist/browser/tracelog.js +2 -2
- package/dist/browser/tracelog.js.map +1 -0
- package/dist/cjs/api.d.ts +1 -1
- package/dist/cjs/api.js +13 -4
- package/dist/cjs/app.d.ts +1 -1
- package/dist/cjs/app.js +4 -4
- package/dist/cjs/constants/config.constants.d.ts +3 -0
- package/dist/cjs/constants/config.constants.js +5 -2
- package/dist/cjs/handlers/click.handler.js +2 -2
- package/dist/cjs/handlers/scroll.handler.js +1 -1
- package/dist/cjs/handlers/session.handler.js +1 -1
- package/dist/cjs/managers/event.manager.d.ts +3 -0
- package/dist/cjs/managers/event.manager.js +47 -6
- package/dist/cjs/managers/sender.manager.js +4 -5
- package/dist/cjs/managers/storage.manager.d.ts +5 -0
- package/dist/cjs/managers/storage.manager.js +95 -6
- package/dist/cjs/public-api.d.ts +1 -1
- package/dist/cjs/test-bridge.d.ts +1 -1
- package/dist/cjs/test-bridge.js +1 -1
- package/dist/cjs/types/config.types.d.ts +4 -4
- package/dist/cjs/types/state.types.d.ts +1 -1
- package/dist/cjs/types/test-bridge.types.d.ts +1 -1
- package/dist/cjs/utils/logging.utils.d.ts +16 -1
- package/dist/cjs/utils/logging.utils.js +65 -4
- package/dist/cjs/utils/network/url.utils.d.ts +1 -1
- package/dist/cjs/utils/network/url.utils.js +11 -12
- package/dist/cjs/utils/validations/config-validations.utils.d.ts +2 -2
- package/dist/cjs/utils/validations/config-validations.utils.js +30 -18
- package/dist/esm/api.d.ts +1 -1
- package/dist/esm/api.js +13 -4
- package/dist/esm/app.d.ts +1 -1
- package/dist/esm/app.js +5 -5
- package/dist/esm/constants/config.constants.d.ts +3 -0
- package/dist/esm/constants/config.constants.js +3 -0
- package/dist/esm/handlers/click.handler.js +2 -2
- package/dist/esm/handlers/scroll.handler.js +1 -1
- package/dist/esm/handlers/session.handler.js +1 -1
- package/dist/esm/managers/event.manager.d.ts +3 -0
- package/dist/esm/managers/event.manager.js +48 -7
- package/dist/esm/managers/sender.manager.js +4 -5
- package/dist/esm/managers/storage.manager.d.ts +5 -0
- package/dist/esm/managers/storage.manager.js +95 -6
- package/dist/esm/public-api.d.ts +1 -1
- package/dist/esm/test-bridge.d.ts +1 -1
- package/dist/esm/test-bridge.js +1 -1
- package/dist/esm/types/config.types.d.ts +4 -4
- package/dist/esm/types/state.types.d.ts +1 -1
- package/dist/esm/types/test-bridge.types.d.ts +1 -1
- package/dist/esm/utils/logging.utils.d.ts +16 -1
- package/dist/esm/utils/logging.utils.js +65 -4
- package/dist/esm/utils/network/url.utils.d.ts +1 -1
- package/dist/esm/utils/network/url.utils.js +9 -10
- package/dist/esm/utils/validations/config-validations.utils.d.ts +2 -2
- package/dist/esm/utils/validations/config-validations.utils.js +30 -18
- package/package.json +7 -6
|
@@ -37,6 +37,9 @@ export class StorageManager {
|
|
|
37
37
|
* Stores an item in storage
|
|
38
38
|
*/
|
|
39
39
|
setItem(key, value) {
|
|
40
|
+
// Always update fallback FIRST for consistency
|
|
41
|
+
// This ensures fallback is in sync and can serve as backup if storage fails
|
|
42
|
+
this.fallbackStorage.set(key, value);
|
|
40
43
|
try {
|
|
41
44
|
if (this.storage) {
|
|
42
45
|
this.storage.setItem(key, value);
|
|
@@ -46,15 +49,37 @@ export class StorageManager {
|
|
|
46
49
|
catch (error) {
|
|
47
50
|
if (error instanceof DOMException && error.name === 'QuotaExceededError') {
|
|
48
51
|
this.hasQuotaExceededError = true;
|
|
49
|
-
log('
|
|
50
|
-
error,
|
|
52
|
+
log('warn', 'localStorage quota exceeded, attempting cleanup', {
|
|
51
53
|
data: { key, valueSize: value.length },
|
|
52
54
|
});
|
|
55
|
+
// Attempt to free up space by removing old TraceLog data
|
|
56
|
+
const cleanedUp = this.cleanupOldData();
|
|
57
|
+
if (cleanedUp) {
|
|
58
|
+
// Retry after cleanup
|
|
59
|
+
try {
|
|
60
|
+
if (this.storage) {
|
|
61
|
+
this.storage.setItem(key, value);
|
|
62
|
+
// Successfully stored after cleanup
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (retryError) {
|
|
67
|
+
log('error', 'localStorage quota exceeded even after cleanup - data will not persist', {
|
|
68
|
+
error: retryError,
|
|
69
|
+
data: { key, valueSize: value.length },
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
log('error', 'localStorage quota exceeded and no data to cleanup - data will not persist', {
|
|
75
|
+
error,
|
|
76
|
+
data: { key, valueSize: value.length },
|
|
77
|
+
});
|
|
78
|
+
}
|
|
53
79
|
}
|
|
54
80
|
// Else: Silent fallback - user already warned in constructor
|
|
81
|
+
// Data is already in fallbackStorage (set at beginning)
|
|
55
82
|
}
|
|
56
|
-
// Always update fallback for consistency
|
|
57
|
-
this.fallbackStorage.set(key, value);
|
|
58
83
|
}
|
|
59
84
|
/**
|
|
60
85
|
* Removes an item from storage
|
|
@@ -108,6 +133,69 @@ export class StorageManager {
|
|
|
108
133
|
hasQuotaError() {
|
|
109
134
|
return this.hasQuotaExceededError;
|
|
110
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Attempts to cleanup old TraceLog data from storage to free up space
|
|
138
|
+
* Returns true if any data was removed, false otherwise
|
|
139
|
+
*/
|
|
140
|
+
cleanupOldData() {
|
|
141
|
+
if (!this.storage) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const tracelogKeys = [];
|
|
146
|
+
const persistedEventsKeys = [];
|
|
147
|
+
// Collect all TraceLog keys
|
|
148
|
+
for (let i = 0; i < this.storage.length; i++) {
|
|
149
|
+
const key = this.storage.key(i);
|
|
150
|
+
if (key?.startsWith('tracelog_')) {
|
|
151
|
+
tracelogKeys.push(key);
|
|
152
|
+
// Prioritize removing old persisted events
|
|
153
|
+
if (key.startsWith('tracelog_persisted_events_')) {
|
|
154
|
+
persistedEventsKeys.push(key);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// First, try to remove old persisted events (usually the largest data)
|
|
159
|
+
if (persistedEventsKeys.length > 0) {
|
|
160
|
+
persistedEventsKeys.forEach((key) => {
|
|
161
|
+
try {
|
|
162
|
+
this.storage.removeItem(key);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// Ignore errors during cleanup
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
// Successfully cleaned up - no need to log in production
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
// If no persisted events, remove non-critical keys
|
|
172
|
+
// Define critical key prefixes that should be preserved
|
|
173
|
+
const criticalPrefixes = ['tracelog_session_', 'tracelog_user_id', 'tracelog_device_id', 'tracelog_config'];
|
|
174
|
+
const nonCriticalKeys = tracelogKeys.filter((key) => {
|
|
175
|
+
// Keep keys that start with any critical prefix
|
|
176
|
+
return !criticalPrefixes.some((prefix) => key.startsWith(prefix));
|
|
177
|
+
});
|
|
178
|
+
if (nonCriticalKeys.length > 0) {
|
|
179
|
+
// Remove up to 5 non-critical keys
|
|
180
|
+
const keysToRemove = nonCriticalKeys.slice(0, 5);
|
|
181
|
+
keysToRemove.forEach((key) => {
|
|
182
|
+
try {
|
|
183
|
+
this.storage.removeItem(key);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// Ignore errors during cleanup
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
// Successfully cleaned up - no need to log in production
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
log('error', 'Failed to cleanup old data', { error });
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
111
199
|
/**
|
|
112
200
|
* Initialize storage (localStorage or sessionStorage) with feature detection
|
|
113
201
|
*/
|
|
@@ -145,6 +233,8 @@ export class StorageManager {
|
|
|
145
233
|
* Stores an item in sessionStorage
|
|
146
234
|
*/
|
|
147
235
|
setSessionItem(key, value) {
|
|
236
|
+
// Always update fallback FIRST for consistency
|
|
237
|
+
this.fallbackSessionStorage.set(key, value);
|
|
148
238
|
try {
|
|
149
239
|
if (this.sessionStorageRef) {
|
|
150
240
|
this.sessionStorageRef.setItem(key, value);
|
|
@@ -159,9 +249,8 @@ export class StorageManager {
|
|
|
159
249
|
});
|
|
160
250
|
}
|
|
161
251
|
// Else: Silent fallback - user already warned in constructor
|
|
252
|
+
// Data is already in fallbackSessionStorage (set at beginning)
|
|
162
253
|
}
|
|
163
|
-
// Always update fallback for consistency
|
|
164
|
-
this.fallbackSessionStorage.set(key, value);
|
|
165
254
|
}
|
|
166
255
|
/**
|
|
167
256
|
* Removes an item from sessionStorage
|
package/dist/esm/public-api.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export * from './app.constants';
|
|
2
2
|
export * from './types';
|
|
3
3
|
export declare const tracelog: {
|
|
4
|
-
init: (config
|
|
4
|
+
init: (config?: import("./types").Config) => Promise<void>;
|
|
5
5
|
event: (name: string, metadata?: Record<string, import("./types").MetadataType> | Record<string, import("./types").MetadataType>[]) => void;
|
|
6
6
|
on: <K extends keyof import("./types").EmitterMap>(event: K, callback: import("./types").EmitterCallback<import("./types").EmitterMap[K]>) => void;
|
|
7
7
|
off: <K extends keyof import("./types").EmitterMap>(event: K, callback: import("./types").EmitterCallback<import("./types").EmitterMap[K]>) => void;
|
|
@@ -16,7 +16,7 @@ export declare class TestBridge extends App implements TraceLogTestBridge {
|
|
|
16
16
|
private _isInitializing;
|
|
17
17
|
private _isDestroying;
|
|
18
18
|
constructor(isInitializing: boolean, isDestroying: boolean);
|
|
19
|
-
init(config
|
|
19
|
+
init(config?: any): Promise<void>;
|
|
20
20
|
isInitializing(): boolean;
|
|
21
21
|
sendCustomEvent(name: string, data?: Record<string, unknown> | Record<string, unknown>[]): void;
|
|
22
22
|
getSessionData(): Record<string, unknown> | null;
|
package/dist/esm/test-bridge.js
CHANGED
|
@@ -74,7 +74,7 @@ export class TestBridge extends App {
|
|
|
74
74
|
throw new Error('Storage manager not available');
|
|
75
75
|
}
|
|
76
76
|
const config = this.get('config');
|
|
77
|
-
const projectId = config?.integrations?.tracelog?.projectId ?? config?.integrations?.custom?.
|
|
77
|
+
const projectId = config?.integrations?.tracelog?.projectId ?? config?.integrations?.custom?.collectApiUrl ?? 'test';
|
|
78
78
|
const userId = this.get('userId');
|
|
79
79
|
const sessionId = this.get('sessionId');
|
|
80
80
|
if (!projectId || !userId) {
|
|
@@ -6,8 +6,6 @@ export interface Config {
|
|
|
6
6
|
globalMetadata?: Record<string, MetadataType>;
|
|
7
7
|
/** Selectors defining custom scroll containers to monitor. */
|
|
8
8
|
scrollContainerSelectors?: string | string[];
|
|
9
|
-
/** Enables HTTP requests for testing and development flows. */
|
|
10
|
-
allowHttp?: boolean;
|
|
11
9
|
/** Query parameters to remove before tracking URLs. */
|
|
12
10
|
sensitiveQueryParams?: string[];
|
|
13
11
|
/** Error event sampling rate between 0 and 1. */
|
|
@@ -23,8 +21,10 @@ export interface Config {
|
|
|
23
21
|
};
|
|
24
22
|
/** Custom integration options. */
|
|
25
23
|
custom?: {
|
|
26
|
-
/**
|
|
27
|
-
|
|
24
|
+
/** Endpoint for collecting events. */
|
|
25
|
+
collectApiUrl: string;
|
|
26
|
+
/** Allow HTTP URLs (not recommended for production). @default false */
|
|
27
|
+
allowHttp?: boolean;
|
|
28
28
|
};
|
|
29
29
|
/** Google Analytics integration options. */
|
|
30
30
|
googleAnalytics?: {
|
|
@@ -15,7 +15,7 @@ import { State } from './state.types';
|
|
|
15
15
|
*/
|
|
16
16
|
export interface TraceLogTestBridge {
|
|
17
17
|
readonly initialized: boolean;
|
|
18
|
-
init(config
|
|
18
|
+
init(config?: Config): Promise<void>;
|
|
19
19
|
destroy(): Promise<void>;
|
|
20
20
|
isInitializing(): boolean;
|
|
21
21
|
sendCustomEvent(name: string, data?: Record<string, unknown> | Record<string, unknown>[]): void;
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
export declare const formatLogMsg: (msg: string, error?: unknown) => string;
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Safe logging utility that respects production environment
|
|
4
|
+
*
|
|
5
|
+
* @param type - Log level (info, warn, error, debug)
|
|
6
|
+
* @param msg - Message to log
|
|
7
|
+
* @param extra - Optional extra data
|
|
8
|
+
*
|
|
9
|
+
* Production behavior:
|
|
10
|
+
* - debug: Never logged in production
|
|
11
|
+
* - info: Only logged if showToClient=true
|
|
12
|
+
* - warn: Always logged (important for debugging production issues)
|
|
13
|
+
* - error: Always logged
|
|
14
|
+
* - Stack traces are sanitized
|
|
15
|
+
* - Data objects are sanitized
|
|
16
|
+
*/
|
|
17
|
+
export declare const log: (type: "info" | "warn" | "error" | "debug", msg: string, extra?: {
|
|
3
18
|
error?: unknown;
|
|
4
19
|
data?: Record<string, unknown>;
|
|
5
20
|
showToClient?: boolean;
|
|
@@ -1,20 +1,81 @@
|
|
|
1
1
|
export const formatLogMsg = (msg, error) => {
|
|
2
2
|
if (error) {
|
|
3
|
+
// In production, sanitize error messages to avoid exposing sensitive paths
|
|
4
|
+
if (process.env.NODE_ENV !== 'dev' && error instanceof Error) {
|
|
5
|
+
// Remove file paths and line numbers from error messages
|
|
6
|
+
const sanitizedMessage = error.message.replace(/\s+at\s+.*$/gm, '').replace(/\(.*?:\d+:\d+\)/g, '');
|
|
7
|
+
return `[TraceLog] ${msg}: ${sanitizedMessage}`;
|
|
8
|
+
}
|
|
3
9
|
return `[TraceLog] ${msg}: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
|
4
10
|
}
|
|
5
11
|
return `[TraceLog] ${msg}`;
|
|
6
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* Safe logging utility that respects production environment
|
|
15
|
+
*
|
|
16
|
+
* @param type - Log level (info, warn, error, debug)
|
|
17
|
+
* @param msg - Message to log
|
|
18
|
+
* @param extra - Optional extra data
|
|
19
|
+
*
|
|
20
|
+
* Production behavior:
|
|
21
|
+
* - debug: Never logged in production
|
|
22
|
+
* - info: Only logged if showToClient=true
|
|
23
|
+
* - warn: Always logged (important for debugging production issues)
|
|
24
|
+
* - error: Always logged
|
|
25
|
+
* - Stack traces are sanitized
|
|
26
|
+
* - Data objects are sanitized
|
|
27
|
+
*/
|
|
7
28
|
export const log = (type, msg, extra) => {
|
|
8
|
-
const { error, data, showToClient } = extra ?? {};
|
|
29
|
+
const { error, data, showToClient = false } = extra ?? {};
|
|
9
30
|
const formattedMsg = error ? formatLogMsg(msg, error) : `[TraceLog] ${msg}`;
|
|
10
31
|
const method = type === 'error' ? 'error' : type === 'warn' ? 'warn' : 'log';
|
|
11
|
-
|
|
12
|
-
|
|
32
|
+
// Production logging strategy:
|
|
33
|
+
// - Development: Log everything
|
|
34
|
+
// - Production:
|
|
35
|
+
// - debug: never logged
|
|
36
|
+
// - info: only if showToClient=true
|
|
37
|
+
// - warn: always logged (critical for debugging)
|
|
38
|
+
// - error: always logged
|
|
39
|
+
const isProduction = process.env.NODE_ENV !== 'dev';
|
|
40
|
+
if (isProduction) {
|
|
41
|
+
// Never log debug in production
|
|
42
|
+
if (type === 'debug') {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// Log info only if explicitly flagged
|
|
46
|
+
if (type === 'info' && !showToClient) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// warn and error always logged in production
|
|
13
50
|
}
|
|
14
|
-
|
|
51
|
+
// In production, sanitize data to avoid exposing sensitive information
|
|
52
|
+
if (isProduction && data !== undefined) {
|
|
53
|
+
const sanitizedData = sanitizeLogData(data);
|
|
54
|
+
console[method](formattedMsg, sanitizedData);
|
|
55
|
+
}
|
|
56
|
+
else if (data !== undefined) {
|
|
15
57
|
console[method](formattedMsg, data);
|
|
16
58
|
}
|
|
17
59
|
else {
|
|
18
60
|
console[method](formattedMsg);
|
|
19
61
|
}
|
|
20
62
|
};
|
|
63
|
+
/**
|
|
64
|
+
* Sanitizes log data in production to prevent sensitive information leakage
|
|
65
|
+
* Simple approach: redact sensitive keys only
|
|
66
|
+
*/
|
|
67
|
+
const sanitizeLogData = (data) => {
|
|
68
|
+
const sanitized = {};
|
|
69
|
+
const sensitiveKeys = ['token', 'password', 'secret', 'key', 'apikey', 'api_key', 'sessionid', 'session_id'];
|
|
70
|
+
for (const [key, value] of Object.entries(data)) {
|
|
71
|
+
const lowerKey = key.toLowerCase();
|
|
72
|
+
// Redact sensitive keys
|
|
73
|
+
if (sensitiveKeys.some((sensitiveKey) => lowerKey.includes(sensitiveKey))) {
|
|
74
|
+
sanitized[key] = '[REDACTED]';
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
sanitized[key] = value;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return sanitized;
|
|
81
|
+
};
|
|
@@ -4,7 +4,7 @@ import { Config } from '@/types';
|
|
|
4
4
|
* @param id - The project ID
|
|
5
5
|
* @returns The generated API URL
|
|
6
6
|
*/
|
|
7
|
-
export declare const
|
|
7
|
+
export declare const getCollectApiUrl: (config: Config) => string;
|
|
8
8
|
/**
|
|
9
9
|
* Normalizes a URL by removing sensitive query parameters
|
|
10
10
|
* @param url - The URL to normalize
|
|
@@ -21,8 +21,7 @@ const isValidUrl = (url, allowHttp = false) => {
|
|
|
21
21
|
* @param id - The project ID
|
|
22
22
|
* @returns The generated API URL
|
|
23
23
|
*/
|
|
24
|
-
export const
|
|
25
|
-
const allowHttp = config.allowHttp ?? false;
|
|
24
|
+
export const getCollectApiUrl = (config) => {
|
|
26
25
|
if (config.integrations?.tracelog?.projectId) {
|
|
27
26
|
const url = new URL(window.location.href);
|
|
28
27
|
const host = url.hostname;
|
|
@@ -32,21 +31,21 @@ export const getApiUrl = (config) => {
|
|
|
32
31
|
}
|
|
33
32
|
const projectId = config.integrations.tracelog.projectId;
|
|
34
33
|
const cleanDomain = parts.slice(-2).join('.');
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const isValid = isValidUrl(apiUrl, allowHttp);
|
|
34
|
+
const collectApiUrl = `https://${projectId}.${cleanDomain}/collect`;
|
|
35
|
+
const isValid = isValidUrl(collectApiUrl);
|
|
38
36
|
if (!isValid) {
|
|
39
37
|
throw new Error('Invalid URL');
|
|
40
38
|
}
|
|
41
|
-
return
|
|
39
|
+
return collectApiUrl;
|
|
42
40
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
41
|
+
const collectApiUrl = config.integrations?.custom?.collectApiUrl;
|
|
42
|
+
if (collectApiUrl) {
|
|
43
|
+
const allowHttp = config.integrations?.custom?.allowHttp ?? false;
|
|
44
|
+
const isValid = isValidUrl(collectApiUrl, allowHttp);
|
|
46
45
|
if (!isValid) {
|
|
47
46
|
throw new Error('Invalid URL');
|
|
48
47
|
}
|
|
49
|
-
return
|
|
48
|
+
return collectApiUrl;
|
|
50
49
|
}
|
|
51
50
|
return '';
|
|
52
51
|
};
|
|
@@ -6,7 +6,7 @@ import { Config } from '../../types';
|
|
|
6
6
|
* @throws {ProjectIdValidationError} If project ID validation fails
|
|
7
7
|
* @throws {AppConfigValidationError} If other configuration validation fails
|
|
8
8
|
*/
|
|
9
|
-
export declare const validateAppConfig: (config
|
|
9
|
+
export declare const validateAppConfig: (config?: Config) => void;
|
|
10
10
|
/**
|
|
11
11
|
* Validates and normalizes the app configuration
|
|
12
12
|
* This is the primary validation entry point that ensures consistent behavior
|
|
@@ -15,4 +15,4 @@ export declare const validateAppConfig: (config: Config) => void;
|
|
|
15
15
|
* @throws {ProjectIdValidationError} If project ID validation fails after normalization
|
|
16
16
|
* @throws {AppConfigValidationError} If other configuration validation fails
|
|
17
17
|
*/
|
|
18
|
-
export declare const validateAndNormalizeConfig: (config
|
|
18
|
+
export declare const validateAndNormalizeConfig: (config?: Config) => Config;
|
|
@@ -9,9 +9,12 @@ import { log } from '../logging.utils';
|
|
|
9
9
|
* @throws {AppConfigValidationError} If other configuration validation fails
|
|
10
10
|
*/
|
|
11
11
|
export const validateAppConfig = (config) => {
|
|
12
|
-
if (
|
|
12
|
+
if (config !== undefined && (config === null || typeof config !== 'object')) {
|
|
13
13
|
throw new AppConfigValidationError('Configuration must be an object', 'config');
|
|
14
14
|
}
|
|
15
|
+
if (!config) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
15
18
|
if (config.sessionTimeout !== undefined) {
|
|
16
19
|
if (typeof config.sessionTimeout !== 'number' ||
|
|
17
20
|
config.sessionTimeout < MIN_SESSION_TIMEOUT_MS ||
|
|
@@ -50,11 +53,6 @@ export const validateAppConfig = (config) => {
|
|
|
50
53
|
throw new SamplingRateValidationError(VALIDATION_MESSAGES.INVALID_SAMPLING_RATE, 'config');
|
|
51
54
|
}
|
|
52
55
|
}
|
|
53
|
-
if (config.allowHttp !== undefined) {
|
|
54
|
-
if (typeof config.allowHttp !== 'boolean') {
|
|
55
|
-
throw new AppConfigValidationError('allowHttp must be a boolean', 'config');
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
56
|
};
|
|
59
57
|
/**
|
|
60
58
|
* Validates CSS selector syntax without executing querySelector (XSS prevention)
|
|
@@ -145,13 +143,21 @@ const validateIntegrations = (integrations) => {
|
|
|
145
143
|
}
|
|
146
144
|
}
|
|
147
145
|
if (integrations.custom) {
|
|
148
|
-
if (!integrations.custom.
|
|
149
|
-
typeof integrations.custom.
|
|
150
|
-
integrations.custom.
|
|
146
|
+
if (!integrations.custom.collectApiUrl ||
|
|
147
|
+
typeof integrations.custom.collectApiUrl !== 'string' ||
|
|
148
|
+
integrations.custom.collectApiUrl.trim() === '') {
|
|
151
149
|
throw new IntegrationValidationError(VALIDATION_MESSAGES.INVALID_CUSTOM_API_URL, 'config');
|
|
152
150
|
}
|
|
153
|
-
if (
|
|
154
|
-
throw new IntegrationValidationError('
|
|
151
|
+
if (integrations.custom.allowHttp !== undefined && typeof integrations.custom.allowHttp !== 'boolean') {
|
|
152
|
+
throw new IntegrationValidationError('allowHttp must be a boolean', 'config');
|
|
153
|
+
}
|
|
154
|
+
const collectApiUrl = integrations.custom.collectApiUrl.trim();
|
|
155
|
+
if (!collectApiUrl.startsWith('http://') && !collectApiUrl.startsWith('https://')) {
|
|
156
|
+
throw new IntegrationValidationError('Custom API URL must start with "http://" or "https://"', 'config');
|
|
157
|
+
}
|
|
158
|
+
const allowHttp = integrations.custom.allowHttp ?? false;
|
|
159
|
+
if (!allowHttp && collectApiUrl.startsWith('http://')) {
|
|
160
|
+
throw new IntegrationValidationError('Custom API URL must use HTTPS in production. Set allowHttp: true in integration config to allow HTTP (not recommended)', 'config');
|
|
155
161
|
}
|
|
156
162
|
}
|
|
157
163
|
if (integrations.googleAnalytics) {
|
|
@@ -177,13 +183,19 @@ const validateIntegrations = (integrations) => {
|
|
|
177
183
|
export const validateAndNormalizeConfig = (config) => {
|
|
178
184
|
validateAppConfig(config);
|
|
179
185
|
const normalizedConfig = {
|
|
180
|
-
...config,
|
|
181
|
-
sessionTimeout: config
|
|
182
|
-
globalMetadata: config
|
|
183
|
-
sensitiveQueryParams: config
|
|
184
|
-
errorSampling: config
|
|
185
|
-
samplingRate: config
|
|
186
|
-
allowHttp: config.allowHttp ?? false,
|
|
186
|
+
...(config ?? {}),
|
|
187
|
+
sessionTimeout: config?.sessionTimeout ?? DEFAULT_SESSION_TIMEOUT,
|
|
188
|
+
globalMetadata: config?.globalMetadata ?? {},
|
|
189
|
+
sensitiveQueryParams: config?.sensitiveQueryParams ?? [],
|
|
190
|
+
errorSampling: config?.errorSampling ?? 1,
|
|
191
|
+
samplingRate: config?.samplingRate ?? 1,
|
|
187
192
|
};
|
|
193
|
+
// Normalize integrations
|
|
194
|
+
if (normalizedConfig.integrations?.custom) {
|
|
195
|
+
normalizedConfig.integrations.custom = {
|
|
196
|
+
...normalizedConfig.integrations.custom,
|
|
197
|
+
allowHttp: normalizedConfig.integrations.custom.allowHttp ?? false,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
188
200
|
return normalizedConfig;
|
|
189
201
|
};
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@tracelog/lib",
|
|
3
3
|
"description": "JavaScript library for web analytics and real-time event tracking",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"version": "0.6.
|
|
5
|
+
"version": "0.6.3",
|
|
6
6
|
"main": "./dist/cjs/public-api.js",
|
|
7
7
|
"module": "./dist/esm/public-api.js",
|
|
8
8
|
"types": "./dist/esm/public-api.d.ts",
|
|
@@ -40,10 +40,11 @@
|
|
|
40
40
|
"test:unit:watch": "vitest",
|
|
41
41
|
"test:coverage": "vitest run --coverage",
|
|
42
42
|
"test:integration": "vitest run --config vitest.integration.config.mjs",
|
|
43
|
-
"serve": "http-server
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
43
|
+
"serve": "http-server docs -p 3000 --cors",
|
|
44
|
+
"docs:setup": "npm run build:browser:dev && cp dist/browser/tracelog.esm.js docs/tracelog.js",
|
|
45
|
+
"docs:dev": "npm run docs:setup && npm run serve",
|
|
46
|
+
"docs:gh-pages": "npm run build:browser && cp dist/browser/tracelog.esm.js docs/tracelog.js",
|
|
47
|
+
"test:e2e": "npm run docs:setup && NODE_ENV=dev playwright test",
|
|
47
48
|
"ci:build": "npm run build:all",
|
|
48
49
|
"prepare": "husky",
|
|
49
50
|
"release": "node scripts/release.js",
|
|
@@ -55,7 +56,7 @@
|
|
|
55
56
|
"changelog:preview": "node scripts/generate-changelog.js --dry-run"
|
|
56
57
|
},
|
|
57
58
|
"dependencies": {
|
|
58
|
-
"web-vitals": "
|
|
59
|
+
"web-vitals": "4.2.4"
|
|
59
60
|
},
|
|
60
61
|
"devDependencies": {
|
|
61
62
|
"@commitlint/config-conventional": "^19.8.1",
|